@@ -, +, @@ author public keys --- Koha/Plugins/Key.pm | 44 ++++++++++ Koha/Plugins/Keys.pm | 86 ++++++++++++++++++++ Koha/Schema/Result/PluginKey.pm | 93 ++++++++++++++++++++++ .../atomicupdate/bug24632-plugin-key-table.perl | 15 ++++ .../atomicupdate/bug24632-plugin-signature.sql | 2 + installer/data/mysql/kohastructure.sql | 9 +++ installer/data/mysql/sysprefs.sql | 1 + .../admin/preferences/enhanced_content.pref | 7 ++ .../prog/en/modules/plugins/plugins-home.tt | 1 + .../prog/en/modules/plugins/plugins-keys.tt | 92 +++++++++++++++++++++ .../prog/en/modules/plugins/plugins-upload.tt | 6 ++ plugins/plugins-keys.pl | 87 ++++++++++++++++++++ plugins/plugins-upload.pl | 22 +++++ 13 files changed, 465 insertions(+) create mode 100644 Koha/Plugins/Key.pm create mode 100644 Koha/Plugins/Keys.pm create mode 100644 Koha/Schema/Result/PluginKey.pm create mode 100644 installer/data/mysql/atomicupdate/bug24632-plugin-key-table.perl create mode 100644 installer/data/mysql/atomicupdate/bug24632-plugin-signature.sql create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-keys.tt create mode 100644 plugins/plugins-keys.pl --- a/Koha/Plugins/Key.pm +++ a/Koha/Plugins/Key.pm @@ -0,0 +1,44 @@ +package Koha::Plugins::Key; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::Plugin::Method - Koha Plugin Method Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'PluginKey'; +} + +1; --- a/Koha/Plugins/Keys.pm +++ a/Koha/Plugins/Keys.pm @@ -0,0 +1,86 @@ +package Koha::Plugins::Keys; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use Koha::Plugins::Key; + +use Crypt::OpenSSL::RSA; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::Plugin::Methods - Koha City Object set class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'PluginKey'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Plugins::Key'; +} + +sub verify_plugin { + my ($class,$args) = @_; + my $plugin_file = $args->{plugin_file}; + my $signature = $args->{signature}; + #FIXME: add data validation and error handling + my $verified; + if ($plugin_file && $signature){ + my @plugin_keys = $class->as_list(); + eval { + foreach my $plugin_key (@plugin_keys){ + my $key_string = $plugin_key->public_key; + if ($key_string){ + my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($key_string); + if ($rsa_pub){ + $rsa_pub->use_sha256_hash; + if ( $rsa_pub->verify($plugin_file, $signature) ){ + $verified = 1; + last; + } + } + } + } + }; + if ($@){ + warn $@; + } + } + return $verified; +} + +1; --- a/Koha/Schema/Result/PluginKey.pm +++ a/Koha/Schema/Result/PluginKey.pm @@ -0,0 +1,93 @@ +use utf8; +package Koha::Schema::Result::PluginKey; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::PluginKey + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("plugin_keys"); + +=head1 ACCESSORS + +=head2 keyid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 name + + data_type: 'varchar' + is_nullable: 0 + size: 100 + +=head2 public_key + + data_type: 'text' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "keyid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "name", + { data_type => "varchar", is_nullable => 0, size => 100 }, + "public_key", + { data_type => "text", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("keyid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("name", ["name"]); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2020-04-30 09:41:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:0FWQcA2u8gTSpoiGU7PVHw + + +sub koha_objects_class { + 'Koha::Plugins::Keys'; +} + +sub koha_object_class { + 'Koha::Plugins::Key'; +} + +1; --- a/installer/data/mysql/atomicupdate/bug24632-plugin-key-table.perl +++ a/installer/data/mysql/atomicupdate/bug24632-plugin-key-table.perl @@ -0,0 +1,15 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + $dbh->do(" + CREATE TABLE IF NOT EXISTS `plugin_keys` ( + `keyid` int(11) NOT NULL auto_increment, -- unique identifier assigned by Koha + `name` varchar(100) NOT NULL, -- short descriptive name to label public key + `public_key` TEXT NOT NULL, -- public key in PEM format + PRIMARY KEY (`keyid`), + CONSTRAINT UNIQUE `name` ( name ) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + + # Always end with this (adjust the bug info) + NewVersion( $DBversion, 24632, "Add table to store plugin provider public keys"); +} --- a/installer/data/mysql/atomicupdate/bug24632-plugin-signature.sql +++ a/installer/data/mysql/atomicupdate/bug24632-plugin-signature.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES +('RequirePluginSignatures','0','','Require Koha plugins to be signed by a trusted author or prevent Koha plugin upload','YesNo'); --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -4475,6 +4475,15 @@ CREATE TABLE `problem_reports` ( CONSTRAINT `problem_reports_ibfk2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +DROP TABLE IF EXISTS `plugin_keys`; +CREATE TABLE `plugin_keys` ( + `keyid` int(11) NOT NULL auto_increment, -- unique identifier assigned by Koha + `name` varchar(100) NOT NULL, -- short descriptive name to label public key + `public_key` TEXT NOT NULL, -- public key in PEM format + PRIMARY KEY (`keyid`), + CONSTRAINT UNIQUE `name` ( name ) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -536,6 +536,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('ReplyToDefault','',NULL,'Use this email address as the replyto in emails','Free'), ('ReportsLog','0',NULL,'If ON, log information about reports.','YesNo'), ('RequestOnOpac','1',NULL,'If ON, globally enables patron holds on OPAC','YesNo'), +('RequirePluginSignatures','0','','Require Koha plugins to be signed by a trusted author or prevent Koha plugin upload','YesNo'), ('RequireStrongPassword','1','','Require a strong login password for staff and patrons','YesNo'), ('ReservesControlBranch','PatronLibrary','ItemHomeLibrary|PatronLibrary','Branch checked for members reservations rights','Choice'), ('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 +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref @@ -388,6 +388,13 @@ Enhanced Content: - for user access to OverDrive.
- If you enable access you must have a SIP connection registered with - OverDrive for patron authentication against Koha + Plugins: + - + - pref: RequirePluginSignatures + choices: + yes: Require + no: "Don't require" + - "Koha plugins to be signed by a trusted author or prevent Koha plugin upload" RecordedBooks: - - Include RecordedBooks availability information with the client secret --- a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-home.tt @@ -53,6 +53,7 @@ + Manage keys [% END %]

Plugins

--- a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-keys.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-keys.tt @@ -0,0 +1,92 @@ +[% USE raw %] +[% USE Asset %] +[% USE KohaDates %] +[% SET footerjs = 1 %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Tools › Plugins › Keys +[% INCLUDE 'doc-head-close.inc' %] + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'prefs-admin-search.inc' %] + + + +
+
+
+
+ +
+ +

Upload plugin key

+
+
+
NOTE: Only PEM file format is supported.
+
    +
  1. + +
  2. +
  3. + +
  4. +
+ + +
+
+ +

Plugin keys

+ [% UNLESS ( plugin_keys ) %] +
No plugin keys are imported
+ [% ELSE %] + + + + [% IF ( CAN_user_plugins_manage ) %] + + [% END %] + + + [% FOREACH plugin_key IN plugin_keys %] + + + [% IF ( CAN_user_plugins_manage ) %] + + [% END %] + [% END %] +
NameActions
+ [% plugin_key.name | html %] + + +
+ [% END %] +
+ +
+
+ +
+ +
+
+ +[% MACRO jsinclude BLOCK %] + [% Asset.js("js/tools-menu.js") | $raw %] +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/plugins/plugins-upload.tt @@ -27,6 +27,7 @@ [% ELSIF ( ERROR.EMPTYUPLOAD ) %]
  • The upload file appears to be empty.
  • [% ELSIF ( ERROR.UZIPFAIL ) %]
  • [% ERROR.UZIPFAIL | html %] failed to unpack.
    Please verify the integrity of the zip file and retry.
  • [% ELSIF ( ERROR.NOWRITEPLUGINS ) %]
  • Cannot unpack file to the plugins directory.
    Please verify that the Apache user can write to the plugins directory.
  • + [% ELSIF ( ERROR.SIGNFAIL ) %]
  • The authenticity of the upload file cannot be verified.
    Please verify that you have uploaded the file's author's public key and the author's signature of the upload file.
  • [% ELSE %]
  • [% ERROR.CORERR | html %] An unknown error has occurred.
    Please review the error log for more details.
  • [% END %] [% END %]
    @@ -39,6 +40,11 @@ +
      +
    1. + +
    2. +
    --- a/plugins/plugins-keys.pl +++ a/plugins/plugins-keys.pl @@ -0,0 +1,87 @@ +#!/usr/bin/perl + +# Copyright 2020 David Cook +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use CGI qw ( -utf8 ); + +use Crypt::OpenSSL::RSA; + +use Koha::Plugins::Key; +use Koha::Plugins::Keys; + +use C4::Auth; +use C4::Output; +use C4::Debug; +use C4::Context; + +my $plugins_enabled = C4::Context->config("enable_plugins"); + +my $input = new CGI; + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { template_name => ($plugins_enabled) ? "plugins/plugins-keys.tt" : "plugins/plugins-disabled.tt", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { plugins => '*' }, + debug => 1, + } +); + +if ($plugins_enabled) { + my $op = $input->param('op') // ''; + if ($op){ + if ($op eq "Upload"){ + my $key_name = $input->param('key_name'); + #my $uploadfilename = $input->param('uploadfile'); + my $uploadfile = $input->upload('uploadfile'); + if ($uploadfile){ + my @lines = <$uploadfile>; + my $key_string = join("",@lines); + if ($key_string){ + my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($key_string); + if ($rsa_pub){ + my $public_key = Koha::Plugins::Key->new({ + name => $key_name, + public_key => $rsa_pub->get_public_key_x509_string(), + }); + if ($public_key){ + $public_key->store(); + } + } + } + } + } + elsif ( $op eq "Delete" ){ + my $keyid = $input->param('keyid'); + if ($keyid){ + my $public_key = Koha::Plugins::Keys->find($keyid); + warn $public_key; + if ($public_key){ + $public_key->delete(); + } + } + } + } + my @plugin_keys = Koha::Plugins::Keys->as_list(); + $template->param( plugin_keys => \@plugin_keys, ); +} + +output_html_with_http_headers( $input, $cookie, $template->output ); --- a/plugins/plugins-upload.pl +++ a/plugins/plugins-upload.pl @@ -23,6 +23,7 @@ use CGI qw ( -utf8 ); use Mojo::UserAgent; use File::Copy; use File::Temp; +use File::Slurp; use C4::Context; use C4::Auth; @@ -30,6 +31,7 @@ use C4::Output; use C4::Members; use C4::Debug; use Koha::Plugins; +use Koha::Plugins::Keys; my $plugins_enabled = C4::Context->config("enable_plugins"); @@ -90,6 +92,26 @@ if ($plugins_enabled) { close $tfh; } + my $verified; + my $uploadsignfile = $input->param('uploadsignfile'); + if ($uploadsignfile){ + local $/ = undef; + my $signature = <$uploadsignfile>; + my $plugin_file = read_file($tempfile, { binmode => ':raw' }); + $verified = Koha::Plugins::Keys->verify_plugin({ + plugin_file => $plugin_file, + signature => $signature, + }); + } + + my $require_signature = C4::Context->preference("RequirePluginSignatures"); + if ($require_signature && ! $verified){ + $errors{SIGNFAIL} = 1; + $template->param( ERRORS => [ \%errors ] ); + output_html_with_http_headers $input, $cookie, $template->output; + exit; + } + my $ae = Archive::Extract->new( archive => $tempfile, type => 'zip' ); unless ( $ae->extract( to => $plugins_dir ) ) { warn "ERROR: " . $ae->error; --