File Coverage

blib/lib/App/MatrixTool/Command/server_key.pm
Criterion Covered Total %
statement 33 115 28.7
branch 0 34 0.0
condition 0 3 0.0
subroutine 10 21 47.6
pod 0 5 0.0
total 43 178 24.1


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-2016 -- leonerd@leonerd.org.uk
5              
6             package App::MatrixTool::Command::server_key;
7              
8 1     1   1058 use strict;
  1         2  
  1         34  
9 1     1   5 use warnings;
  1         2  
  1         35  
10 1     1   5 use base qw( App::MatrixTool );
  1         2  
  1         106  
11              
12             our $VERSION = '0.07';
13              
14 1     1   6 use MIME::Base64 qw( decode_base64 );
  1         1  
  1         69  
15 1     1   7 use Protocol::Matrix qw( verify_json_signature );
  1         2  
  1         61  
16              
17 1     1   809 use Net::Async::HTTP;
  1         34048  
  1         106  
18             Net::Async::HTTP->VERSION( '0.40' ); # ->request on_ready
19              
20 1         2 use constant SHA256_ALGO => do {
21 1         828 require Net::SSLeay;
22 1         10237 Net::SSLeay::SSLeay_add_ssl_algorithms();
23 1         135 Net::SSLeay::EVP_get_digestbyname( "sha256" );
24 1     1   10 };
  1         4  
25              
26 1     1   11 use constant DESCRIPTION => "Fetch a server's signing key";
  1         1  
  1         74  
27 1     1   7 use constant ARGUMENTS => ( "server_name" );
  1         1  
  1         87  
28 1         1712 use constant OPTIONS => (
29             '1|v1' => "Restrict to the v1 key API",
30             '2|v2' => "Restrict to the v2 key API",
31             'n|no-store' => "Don't cache the received key in the key store",
32 1     1   7 );
  1         2  
33              
34             =head1 NAME
35              
36             matrixtool server-key - Fetch a server's signing key
37              
38             =head1 SYNOPSIS
39              
40             $ matrixtool server-key my-server.org
41              
42             =head1 DESCRIPTION
43              
44             This command fetches the keys from a Matrix homeserver. This helps you test
45             whether the server is basically configured correctly, responding to basic
46             federation key requests.
47              
48             =head1 OPTIONS
49              
50             The following additional options are recognised
51              
52             =over 4
53              
54             =item C<--v1>, C<-1>
55              
56             Restrict to the v1 key API
57              
58             =item C<--v2>, C<-2>
59              
60             Restrict to the v2 key API
61              
62             =item C<--no-store>, C<-n>
63              
64             Don't cache the received key in the key store
65              
66             =back
67              
68             =cut
69              
70             sub get_key_v1
71             {
72 0     0 0   my $self = shift;
73 0           my ( $server_name ) = @_;
74              
75 0           my $server_cert;
76              
77             $self->http_client->request_json(
78             server => $server_name,
79             request => $self->federation->make_key_v1_request(
80             server_name => $server_name,
81             ),
82              
83             on_ready => sub {
84 0     0     my ( $conn ) = @_;
85 0           my $socket = $conn->read_handle;
86              
87 0           $server_cert = $socket->peer_certificate;
88              
89 0           $self->output_info( "Connected to " . $self->format_addr( $socket->peername ) );
90 0           Future->done;
91             },
92             )->then( sub {
93 0     0     my ( $body ) = @_;
94              
95 0 0         $body->{server_name} eq $server_name or
96             $self->output_check_failure( "Returned server_name does not match" );
97              
98             # Ugh SSLeay is a pain
99 0           my $bio = Net::SSLeay::BIO_new( Net::SSLeay::BIO_s_mem() );
100 0           Net::SSLeay::BIO_write( $bio, decode_base64 $body->{tls_certificate} );
101              
102 0 0         my $got_cert = Net::SSLeay::d2i_X509_bio( $bio ) or
103             die Net::SSLeay::print_errs("d2i_X509_bio");
104              
105 0 0         Net::SSLeay::X509_digest( $server_cert, SHA256_ALGO ) eq Net::SSLeay::X509_digest( $got_cert, SHA256_ALGO )
106             ? $self->output_ok( "TLS certificate fingerprint matches (SHA-256)" )
107             : $self->output_check_failure( "TLS certificate fingerprint does not match" );
108              
109 0           my $keys = $body->{verify_keys};
110 0           my @keys = map { { id => $_, key => decode_base64 $keys->{$_} } } sort keys %$keys;
  0            
111              
112 0           $self->verify( $body, @keys );
113              
114             Future->done(
115             version => "v1",
116             server_name => $body->{server_name},
117 0           keys => \@keys,
118             );
119 0           });
120             }
121              
122             sub get_key_v2
123             {
124 0     0 0   my $self = shift;
125 0           my ( $server_name ) = @_;
126              
127 0           my $server_cert;
128              
129             $self->http_client->request_json(
130             server => $server_name,
131             request => $self->federation->make_key_v2_server_request(
132             server_name => $server_name,
133             key_id => "*",
134             ),
135              
136             on_ready => sub {
137 0     0     my ( $conn ) = @_;
138 0           my $socket = $conn->read_handle;
139              
140 0           $server_cert = $socket->peer_certificate;
141              
142 0           $self->output_info( "Connected to " . $self->format_addr( $socket->peername ) );
143 0           Future->done;
144             },
145             )->then( sub {
146 0     0     my ( $body ) = @_;
147              
148 0           my $fingerprint = Net::SSLeay::X509_digest( $server_cert, SHA256_ALGO );
149              
150 0 0         $body->{server_name} eq $server_name or
151             $self->output_check_failure( "Returned server_name does not match" );
152              
153 0           my $fprint_ok;
154 0           foreach ( @{ $body->{tls_fingerprints} } ) {
  0            
155 0 0         $_->{sha256} or next;
156 0 0         decode_base64( $_->{sha256} ) eq $fingerprint and $fprint_ok++, last;
157             }
158 0 0         $fprint_ok ? $self->output_ok( "TLS fingerprint matches (SHA-256)" )
159             : $self->output_check_failure( "TLS fingerprint does not match any listed" );
160              
161 0           my $keys = $body->{verify_keys};
162 0           my @keys = map { { id => $_, key => decode_base64 $keys->{$_}{key} } } sort keys %$keys;
  0            
163              
164 0           $self->verify( $body, @keys );
165              
166             Future->done(
167             version => "v2",
168             server_name => $body->{server_name},
169 0           keys => \@keys,
170             );
171 0           });
172             }
173              
174             sub verify
175             {
176 0     0 0   my $self = shift;
177 0           my ( $body, @keys ) = @_;
178              
179 0           my %keys_by_id = map { $_->{id} => $_->{key} } @keys;
  0            
180              
181 0           my $ok;
182 0           foreach my $origin ( sort keys %{ $body->{signatures} } ) {
  0            
183 0           foreach my $key_id ( sort keys %{ $body->{signatures}{$origin} } ) {
  0            
184 0 0         my $key = $keys_by_id{$key_id} or do {
185 0           $self->output_info( "Skipping origin=$origin key_id=$key_id as there is no useable public key" );
186 0           next;
187             };
188              
189 0           my $verified = eval { verify_json_signature( $body,
  0            
190             public_key => $key,
191             origin => $origin,
192             key_id => $key_id,
193 0           ); 1 };
194              
195 0 0         $verified or
196             $self->output_check_failure( "Signature verification failed for origin=$origin key_id=$key_id" );
197              
198 0 0         $verified and $ok++,
199             $self->output_ok( "Verified using origin=$origin key_id=$key_id" );
200             }
201             }
202              
203             $ok or
204 0 0         $self->output_check_failure( "Failed to find any valid signatures" );
205             }
206              
207             sub output_check_failure
208             {
209 0     0 0   my $self = shift;
210             # TODO: option to make this fatal or non-fatal
211 0           $self->output_fail( @_ );
212             }
213              
214             sub run
215             {
216 0     0 0   my $self = shift;
217 0           my ( $opts, $server_name ) = @_;
218              
219             $opts->{v1} and $opts->{v2} and
220 0 0 0       return $self->error( "Cannot request 'v1' and 'v2' key API at the same time" );
221              
222             ( $opts->{v1} ? Future->fail( "No v2" ) : $self->get_key_v2( $server_name ) )
223 0 0   0     ->else_with_f( sub { $opts->{v2} ? shift : $self->get_key_v1( $server_name ) } )
224             ->then( sub {
225 0     0     my %result = @_;
226 0           $self->output( "$result{version} keys from $result{server_name}:" );
227 0           $self->output();
228              
229 0           my $store = $self->server_key_store;
230 0           my %cached_keys = $store->list( server => $result{server_name} );
231              
232 0           foreach ( @{ $result{keys} } ) {
  0            
233 0           $self->output( "Key id $_->{id}" );
234 0           $self->output( " " . $self->format_binary( $_->{key} ) );
235              
236 0 0         if( !exists $cached_keys{ $_->{id} } ) {
    0          
237             $store->put(
238             server => $result{server_name},
239             id => $_->{id},
240             data => $_->{key},
241 0 0         ) unless $opts->{no_store};
242             }
243             elsif( $cached_keys{ $_->{id} } eq $_->{key} ) {
244 0           $self->output_info( "Matches cached key" );
245             }
246             else {
247 0           $self->output_warn( "Does not match cached key " . $self->format_binary( $cached_keys{ $_->{id} } ) );
248             }
249             }
250              
251 0           Future->done;
252 0 0         });
253             }
254              
255             =head1 EXAMPLES
256              
257             For example, fetching the keys from a server:
258              
259             $ matrixtool server-key matrix.org
260             [INFO] Connected to 83.166.64.33:8448
261             Keys from matrix.org
262              
263             Key id ed25519:auto
264             base64::aBcDeFgHiJ...
265             [INFO] Matches cached key
266              
267             =cut
268              
269             =head1 AUTHOR
270              
271             Paul Evans
272              
273             =cut
274              
275             0x55AA;