View | Details | Raw Unified | Return to bug 24632
Collapse All | Expand All

(-)a/Koha/Plugins/Key.pm (+44 lines)
Line 0 Link Here
1
package Koha::Plugins::Key;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Carp;
21
22
use Koha::Database;
23
24
use base qw(Koha::Object);
25
26
=head1 NAME
27
28
Koha::Plugin::Method - Koha Plugin Method Object class
29
30
=head1 API
31
32
=head2 Class Methods
33
34
=cut
35
36
=head3 type
37
38
=cut
39
40
sub _type {
41
    return 'PluginKey';
42
}
43
44
1;
(-)a/Koha/Plugins/Keys.pm (+86 lines)
Line 0 Link Here
1
package Koha::Plugins::Keys;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Carp;
21
22
use Koha::Database;
23
24
use Koha::Plugins::Key;
25
26
use Crypt::OpenSSL::RSA;
27
28
use base qw(Koha::Objects);
29
30
=head1 NAME
31
32
Koha::Plugin::Methods - Koha City Object set class
33
34
=head1 API
35
36
=head2 Class Methods
37
38
=cut
39
40
=head3 type
41
42
=cut
43
44
sub _type {
45
    return 'PluginKey';
46
}
47
48
=head3 object_class
49
50
=cut
51
52
sub object_class {
53
    return 'Koha::Plugins::Key';
54
}
55
56
sub verify_plugin {
57
	my ($class,$args) = @_;
58
    my $plugin_file = $args->{plugin_file};
59
    my $signature = $args->{signature};
60
    #FIXME: add data validation and error handling
61
	my $verified;
62
    if ($plugin_file && $signature){
63
        my @plugin_keys = $class->as_list();
64
        eval {
65
            foreach my $plugin_key (@plugin_keys){
66
                my $key_string = $plugin_key->public_key;
67
                if ($key_string){
68
                    my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($key_string);
69
                    if ($rsa_pub){
70
                        $rsa_pub->use_sha256_hash;
71
                        if ( $rsa_pub->verify($plugin_file, $signature) ){
72
                            $verified = 1;
73
                            last;
74
                        }
75
                    }
76
                }
77
            }
78
        };
79
        if ($@){
80
            warn $@;
81
        }
82
    }
83
	return $verified;
84
}
85
86
1;
(-)a/Koha/Schema/Result/PluginKey.pm (+93 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::PluginKey;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::PluginKey
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<plugin_keys>
19
20
=cut
21
22
__PACKAGE__->table("plugin_keys");
23
24
=head1 ACCESSORS
25
26
=head2 keyid
27
28
  data_type: 'integer'
29
  is_auto_increment: 1
30
  is_nullable: 0
31
32
=head2 name
33
34
  data_type: 'varchar'
35
  is_nullable: 0
36
  size: 100
37
38
=head2 public_key
39
40
  data_type: 'text'
41
  is_nullable: 0
42
43
=cut
44
45
__PACKAGE__->add_columns(
46
  "keyid",
47
  { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
48
  "name",
49
  { data_type => "varchar", is_nullable => 0, size => 100 },
50
  "public_key",
51
  { data_type => "text", is_nullable => 0 },
52
);
53
54
=head1 PRIMARY KEY
55
56
=over 4
57
58
=item * L</keyid>
59
60
=back
61
62
=cut
63
64
__PACKAGE__->set_primary_key("keyid");
65
66
=head1 UNIQUE CONSTRAINTS
67
68
=head2 C<name>
69
70
=over 4
71
72
=item * L</name>
73
74
=back
75
76
=cut
77
78
__PACKAGE__->add_unique_constraint("name", ["name"]);
79
80
81
# Created by DBIx::Class::Schema::Loader v0.07046 @ 2020-04-30 09:41:07
82
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:0FWQcA2u8gTSpoiGU7PVHw
83
84
85
sub koha_objects_class {
86
    'Koha::Plugins::Keys';
87
}
88
89
sub koha_object_class {
90
    'Koha::Plugins::Key';
91
}
92
93
1;
(-)a/installer/data/mysql/atomicupdate/bug24632-plugin-key-table.perl (+15 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do("
4
            CREATE TABLE IF NOT EXISTS `plugin_keys` (
5
                `keyid` int(11) NOT NULL auto_increment, -- unique identifier assigned by Koha
6
                `name` varchar(100) NOT NULL, -- short descriptive name to label public key
7
                `public_key` TEXT NOT NULL, -- public key in PEM format
8
                PRIMARY KEY (`keyid`),
9
                CONSTRAINT UNIQUE `name` ( name )
10
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
11
            ");
12
13
    # Always end with this (adjust the bug info)
14
    NewVersion( $DBversion, 24632, "Add table to store plugin provider public keys");
15
}
(-)a/installer/data/mysql/atomicupdate/bug24632-plugin-signature.sql (+2 lines)
Line 0 Link Here
1
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES
2
('RequirePluginSignatures','0','','Require Koha plugins to be signed by a trusted author or prevent Koha plugin upload','YesNo');
(-)a/installer/data/mysql/kohastructure.sql (+9 lines)
Lines 4475-4480 CREATE TABLE `problem_reports` ( Link Here
4475
    CONSTRAINT `problem_reports_ibfk2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE
4475
    CONSTRAINT `problem_reports_ibfk2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE
4476
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4476
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4477
4477
4478
DROP TABLE IF EXISTS `plugin_keys`;
4479
CREATE TABLE `plugin_keys` (
4480
    `keyid` int(11) NOT NULL auto_increment, -- unique identifier assigned by Koha
4481
    `name` varchar(100) NOT NULL, -- short descriptive name to label public key
4482
    `public_key` TEXT NOT NULL, -- public key in PEM format
4483
    PRIMARY KEY (`keyid`),
4484
    CONSTRAINT UNIQUE `name` ( name )
4485
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4486
4478
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
4487
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
4479
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
4488
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
4480
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
4489
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
(-)a/installer/data/mysql/sysprefs.sql (+1 lines)
Lines 536-541 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
536
('ReplyToDefault','',NULL,'Use this email address as the replyto in emails','Free'),
536
('ReplyToDefault','',NULL,'Use this email address as the replyto in emails','Free'),
537
('ReportsLog','0',NULL,'If ON, log information about reports.','YesNo'),
537
('ReportsLog','0',NULL,'If ON, log information about reports.','YesNo'),
538
('RequestOnOpac','1',NULL,'If ON, globally enables patron holds on OPAC','YesNo'),
538
('RequestOnOpac','1',NULL,'If ON, globally enables patron holds on OPAC','YesNo'),
539
('RequirePluginSignatures','0','','Require Koha plugins to be signed by a trusted author or prevent Koha plugin upload','YesNo'),
539
('RequireStrongPassword','1','','Require a strong login password for staff and patrons','YesNo'),
540
('RequireStrongPassword','1','','Require a strong login password for staff and patrons','YesNo'),
540
('ReservesControlBranch','PatronLibrary','ItemHomeLibrary|PatronLibrary','Branch checked for members reservations rights','Choice'),
541
('ReservesControlBranch','PatronLibrary','ItemHomeLibrary|PatronLibrary','Branch checked for members reservations rights','Choice'),
541
('ReservesMaxPickUpDelay','7','','Define the Maximum delay to pick up an item on hold','Integer'),
542
('ReservesMaxPickUpDelay','7','','Define the Maximum delay to pick up an item on hold','Integer'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref (+7 lines)
Lines 388-393 Enhanced Content: Link Here
388
            - for user access to OverDrive. <br />
388
            - for user access to OverDrive. <br />
389
            - If you enable access you must have a SIP connection registered with
389
            - If you enable access you must have a SIP connection registered with
390
            - OverDrive for patron authentication against Koha
390
            - OverDrive for patron authentication against Koha
391
    Plugins:
392
        -
393
            - pref: RequirePluginSignatures
394
              choices:
395
                  yes: Require
396
                  no: "Don't require"
397
            - "Koha plugins to be signed by a trusted author or prevent Koha plugin upload"
391
    RecordedBooks:
398
    RecordedBooks:
392
        -
399
        -
393
            - Include RecordedBooks availability information with the client secret
400
            - Include RecordedBooks availability information with the client secret
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt (+1 lines)
Lines 53-58 Link Here
53
                                    </div>
53
                                    </div>
54
                                </form>
54
                                </form>
55
                            </div>
55
                            </div>
56
                            <a href="/cgi-bin/koha/plugins/plugins-keys.pl" id="plugin_keys" class="btn btn-default"><i class="fa fa-key"></i> Manage keys</a>
56
                        </div>
57
                        </div>
57
                    [% END %]
58
                    [% END %]
58
                    <h1>Plugins</h1>
59
                    <h1>Plugins</h1>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-keys.tt (+92 lines)
Line 0 Link Here
1
[% USE raw %]
2
[% USE Asset %]
3
[% USE KohaDates %]
4
[% SET footerjs = 1 %]
5
[% INCLUDE 'doc-head-open.inc' %]
6
<title>Koha &rsaquo; Tools &rsaquo; Plugins &rsaquo; Keys</title>
7
[% INCLUDE 'doc-head-close.inc' %]
8
</head>
9
10
<body id="plugins_plugins_keys" class="plugins">
11
[% INCLUDE 'header.inc' %]
12
[% INCLUDE 'prefs-admin-search.inc' %]
13
14
<div id="breadcrumbs">
15
    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
16
    <a href="/cgi-bin/koha/plugins/plugins-home.pl">Plugins</a> &rsaquo;
17
    Keys
18
</div>
19
20
<div class="main container-fluid">
21
    <div class="row">
22
        <div class="col-sm-10 col-sm-push-2">
23
            <main>
24
25
                <div class="details">
26
27
                    <h1>Upload plugin key</h1>
28
					<form method="post" action="/cgi-bin/koha/plugins/plugins-keys.pl" enctype="multipart/form-data">
29
                    <fieldset class="brief">
30
                          <div class="hint"><b>NOTE:</b> Only PEM file format is supported.</div>
31
                         <ol>
32
                            <li>
33
                                <label for="key_name">Name: </label><input type="text" size="50" id="key_name" name="key_name"/>
34
                            </li>
35
                             <li>
36
                                 <label for="uploadfile">Select the file to upload: </label><input type="file" id="uploadfile" name="uploadfile" />
37
                             </li>
38
                         </ol>
39
                         <input type="hidden" name="op" value="Upload" />
40
                         <input type="submit" value="Upload" class="submit" />
41
                     </fieldset>
42
                 	</form>
43
44
                    <h1>Plugin keys</h1>
45
                    [% UNLESS ( plugin_keys ) %]
46
                        <div class="dialog message">No plugin keys are imported</div>
47
                    [% ELSE %]
48
                        <table>
49
                            <tr>
50
                                <th>Name</th>
51
                                [% IF ( CAN_user_plugins_manage ) %]
52
                                    <th>Actions</th>
53
                                [% END %]
54
                            </tr>
55
56
                            [% FOREACH plugin_key IN plugin_keys %]
57
                                <tr>
58
                                    <td>
59
                                        <strong>[% plugin_key.name | html %]</strong>
60
                                    </td>
61
                                    [% IF ( CAN_user_plugins_manage ) %]
62
                                        <td class="actions">
63
                                            <div class="btn-group dropup">
64
                                                <a class="btn btn-default btn-xs dropdown-toggle" id="pluginactions[% plugin.class | html %]" role="button" data-toggle="dropdown" href="#">
65
                                                   Actions <b class="caret"></b>
66
                                                </a>
67
                                                <ul class="dropdown-menu pull-right" role="menu" aria-labelledby="pluginactions[% plugin.class | html %]">
68
                                                    <li><a class="uninstall_plugin" href="/cgi-bin/koha/plugins/plugins-keys.pl?keyid=[% plugin_key.keyid %]&op=Delete"><i class="fa fa-trash fa-fw"></i> Delete</a></li>
69
                                                </ul>
70
                                            </div>
71
                                        </td>
72
                                    [% END %]
73
                            [% END %]
74
                        </table>
75
                    [% END %]
76
                </div>
77
78
            </main>
79
        </div> <!-- /.col-sm-10.col-sm-push-2 -->
80
81
        <div class="col-sm-2 col-sm-pull-10">
82
            <aside>
83
                [% INCLUDE 'tools-menu.inc' %]
84
            </aside>
85
        </div> <!-- /.col-sm-2.col-sm-pull-10 -->
86
     </div> <!-- /.row -->
87
88
[% MACRO jsinclude BLOCK %]
89
    [% Asset.js("js/tools-menu.js") | $raw %]
90
[% END %]
91
92
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt (+6 lines)
Lines 27-32 Link Here
27
                        [% ELSIF ( ERROR.EMPTYUPLOAD ) %]<li><b>The upload file appears to be empty.</b></li>
27
                        [% ELSIF ( ERROR.EMPTYUPLOAD ) %]<li><b>The upload file appears to be empty.</b></li>
28
                        [% ELSIF ( ERROR.UZIPFAIL ) %]<li><b>[% ERROR.UZIPFAIL | html %] failed to unpack.<br />Please verify the integrity of the zip file and retry.</b></li>
28
                        [% ELSIF ( ERROR.UZIPFAIL ) %]<li><b>[% ERROR.UZIPFAIL | html %] failed to unpack.<br />Please verify the integrity of the zip file and retry.</b></li>
29
                        [% ELSIF ( ERROR.NOWRITEPLUGINS ) %]<li><b>Cannot unpack file to the plugins directory.<br />Please verify that the Apache user can write to the plugins directory.</b></li>
29
                        [% ELSIF ( ERROR.NOWRITEPLUGINS ) %]<li><b>Cannot unpack file to the plugins directory.<br />Please verify that the Apache user can write to the plugins directory.</b></li>
30
                        [% ELSIF ( ERROR.SIGNFAIL ) %]<li><b>The authenticity of the upload file cannot be verified.<br />Please verify that you have uploaded the file's author's public key and the author's signature of the upload file.</b></li>
30
                        [% ELSE %]<li><b>[% ERROR.CORERR | html %] An unknown error has occurred.<br />Please review the error log for more details.</b></li>[% END %]
31
                        [% ELSE %]<li><b>[% ERROR.CORERR | html %] An unknown error has occurred.<br />Please review the error log for more details.</b></li>[% END %]
31
                    [% END %]
32
                    [% END %]
32
                </div>
33
                </div>
Lines 39-44 Link Here
39
                                <label for="uploadfile">Select the file to upload: </label><input type="file" id="uploadfile" name="uploadfile" />
40
                                <label for="uploadfile">Select the file to upload: </label><input type="file" id="uploadfile" name="uploadfile" />
40
                            </li>
41
                            </li>
41
                        </ol>
42
                        </ol>
43
                        <ol>
44
                            <li>
45
                                <label for="uploadsignfile">Select the signature to upload: </label><input type="file" id="uploadsignfile" name="uploadsignfile" />
46
                            </li>
47
                        </ol>
42
                    </fieldset>
48
                    </fieldset>
43
                    <fieldset class="action">
49
                    <fieldset class="action">
44
                        <input type="hidden" name="op" value="Upload" />
50
                        <input type="hidden" name="op" value="Upload" />
(-)a/plugins/plugins-keys.pl (+87 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2020 David Cook <dcook@prosentient.com.au>
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use CGI qw ( -utf8 );
23
24
use Crypt::OpenSSL::RSA;
25
26
use Koha::Plugins::Key;
27
use Koha::Plugins::Keys;
28
29
use C4::Auth;
30
use C4::Output;
31
use C4::Debug;
32
use C4::Context;
33
34
my $plugins_enabled = C4::Context->config("enable_plugins");
35
36
my $input  = new CGI;
37
38
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
39
    {   template_name => ($plugins_enabled) ? "plugins/plugins-keys.tt" : "plugins/plugins-disabled.tt",
40
        query         => $input,
41
        type          => "intranet",
42
        authnotrequired => 0,
43
        flagsrequired   => { plugins => '*' },
44
        debug           => 1,
45
    }
46
);
47
48
if ($plugins_enabled) {
49
    my $op = $input->param('op') // '';
50
    if ($op){
51
        if ($op eq "Upload"){
52
            my $key_name = $input->param('key_name');
53
            #my $uploadfilename = $input->param('uploadfile');
54
            my $uploadfile     = $input->upload('uploadfile');
55
            if ($uploadfile){
56
                my @lines = <$uploadfile>;
57
                my $key_string = join("",@lines);
58
                if ($key_string){
59
                    my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($key_string);
60
                    if ($rsa_pub){
61
                        my $public_key = Koha::Plugins::Key->new({
62
                            name => $key_name,
63
                            public_key => $rsa_pub->get_public_key_x509_string(),
64
                        });
65
                        if ($public_key){
66
                            $public_key->store();
67
                        }
68
                    }
69
                }
70
            }
71
        }
72
        elsif ( $op eq "Delete" ){
73
            my $keyid = $input->param('keyid');
74
            if ($keyid){
75
                my $public_key = Koha::Plugins::Keys->find($keyid);
76
                warn $public_key;
77
                if ($public_key){
78
                    $public_key->delete();
79
                }
80
            }
81
        }
82
    }
83
    my @plugin_keys = Koha::Plugins::Keys->as_list();
84
    $template->param( plugin_keys => \@plugin_keys, );
85
}
86
87
output_html_with_http_headers( $input, $cookie, $template->output );
(-)a/plugins/plugins-upload.pl (-1 / +22 lines)
Lines 23-28 use CGI qw ( -utf8 ); Link Here
23
use Mojo::UserAgent;
23
use Mojo::UserAgent;
24
use File::Copy;
24
use File::Copy;
25
use File::Temp;
25
use File::Temp;
26
use File::Slurp;
26
27
27
use C4::Context;
28
use C4::Context;
28
use C4::Auth;
29
use C4::Auth;
Lines 30-35 use C4::Output; Link Here
30
use C4::Members;
31
use C4::Members;
31
use C4::Debug;
32
use C4::Debug;
32
use Koha::Plugins;
33
use Koha::Plugins;
34
use Koha::Plugins::Keys;
33
35
34
my $plugins_enabled = C4::Context->config("enable_plugins");
36
my $plugins_enabled = C4::Context->config("enable_plugins");
35
37
Lines 90-95 if ($plugins_enabled) { Link Here
90
                close $tfh;
92
                close $tfh;
91
            }
93
            }
92
94
95
            my $verified;
96
            my $uploadsignfile = $input->param('uploadsignfile');
97
            if ($uploadsignfile){
98
                local $/ = undef;
99
                my $signature = <$uploadsignfile>;
100
                my $plugin_file = read_file($tempfile, { binmode => ':raw' });
101
                $verified = Koha::Plugins::Keys->verify_plugin({
102
                    plugin_file => $plugin_file,
103
                    signature => $signature,
104
                });
105
            }
106
107
            my $require_signature = C4::Context->preference("RequirePluginSignatures");
108
            if ($require_signature && ! $verified){
109
                $errors{SIGNFAIL} = 1;
110
                $template->param( ERRORS => [ \%errors ] );
111
                output_html_with_http_headers $input, $cookie, $template->output;
112
                exit;
113
            }
114
93
            my $ae = Archive::Extract->new( archive => $tempfile, type => 'zip' );
115
            my $ae = Archive::Extract->new( archive => $tempfile, type => 'zip' );
94
            unless ( $ae->extract( to => $plugins_dir ) ) {
116
            unless ( $ae->extract( to => $plugins_dir ) ) {
95
                warn "ERROR: " . $ae->error;
117
                warn "ERROR: " . $ae->error;
96
- 

Return to bug 24632