From aba646f59bf9c45d4cae063bc1dc5e7da0fa1ee5 Mon Sep 17 00:00:00 2001 From: Lars Dieckow Date: Fri, 4 Oct 2013 14:58:00 +0200 Subject: [PATCH] MT#3929 certificate management --- Build.PL | 1 + lib/NGCP/Panel/Controller/Administrator.pm | 42 ++++++ .../Panel/Form/Administrator/APIDownDelete.pm | 34 +++++ .../Panel/Form/Administrator/APIGenerate.pm | 26 ++++ lib/NGCP/Panel/Model/CA.pm | 131 ++++++++++++++++++ share/static/css/main.css | 3 + share/templates/administrator/list.tt | 13 ++ 7 files changed, 250 insertions(+) create mode 100644 lib/NGCP/Panel/Form/Administrator/APIDownDelete.pm create mode 100644 lib/NGCP/Panel/Form/Administrator/APIGenerate.pm create mode 100644 lib/NGCP/Panel/Model/CA.pm diff --git a/Build.PL b/Build.PL index 4c7c31a97f..b9bf8367fa 100644 --- a/Build.PL +++ b/Build.PL @@ -48,6 +48,7 @@ my $builder = Local::Module::Build->new( 'HTML::FormHandler::Model::DBIC' => 0, 'HTML::FormHandler::Moose' => 0, 'HTML::FormHandler::Widget::Block::Bootstrap' => 0, + 'HTTP::Headers' => 0, 'HTTP::Status' => 0, 'IPC::System::Simple' => 0, 'Log::Log4perl::Catalyst' => 0, diff --git a/lib/NGCP/Panel/Controller/Administrator.pm b/lib/NGCP/Panel/Controller/Administrator.pm index 51795f61b8..6d806ad82e 100644 --- a/lib/NGCP/Panel/Controller/Administrator.pm +++ b/lib/NGCP/Panel/Controller/Administrator.pm @@ -2,8 +2,11 @@ package NGCP::Panel::Controller::Administrator; use Sipwise::Base; use namespace::sweep; BEGIN { extends 'Catalyst::Controller'; } +use HTTP::Headers qw(); use NGCP::Panel::Form::Administrator::Reseller; use NGCP::Panel::Form::Administrator::Admin; +use NGCP::Panel::Form::Administrator::APIGenerate qw(); +use NGCP::Panel::Form::Administrator::APIDownDelete qw(); use NGCP::Panel::Utils::Message; use NGCP::Panel::Utils::Navigation; @@ -219,6 +222,45 @@ sub delete :Chained('base') :PathPart('delete') :Args(0) { NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/administrator')); } +sub api_key :Chained('base') :PathPart('api_key') :Args(0) { + my ($self, $c) = @_; + my $serial = $c->stash->{administrator}->ssl_client_m_serial; + if ($c->req->body_parameters->{'key_actions.generate'}) { + $serial = time; + my $updated; + while (!$updated) { + try { + $c->stash->{administrator}->update({ ssl_client_m_serial => $serial }); + $updated = 1; + } catch(DBIx::Class::Exception $e where { "$_" =~ qr'Duplicate entry' }) { + $serial++; + }; + } + $c->model('CA')->make_client($serial); + } elsif ($c->req->body_parameters->{'key_actions.delete'}) { + undef $serial; + $c->stash->{administrator}->update({ ssl_client_m_serial => $serial }); + } elsif ($c->req->body_parameters->{'key_actions.download'}) { + my $cert_file = $c->model('CA')->client_cert_file($serial); + $c->res->headers(HTTP::Headers->new( + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => sprintf('attachment; filename=%s', $cert_file->basename) + )); + $c->res->body($cert_file->openr); + return; + } + my $form; + if ($serial) { + $form = NGCP::Panel::Form::Administrator::APIDownDelete->new; + } else { + $form = NGCP::Panel::Form::Administrator::APIGenerate->new; + } + $c->stash( + api_modal_flag => 1, + form => $form, + ); +} + $CLASS->meta->make_immutable; __END__ diff --git a/lib/NGCP/Panel/Form/Administrator/APIDownDelete.pm b/lib/NGCP/Panel/Form/Administrator/APIDownDelete.pm new file mode 100644 index 0000000000..07ec62e035 --- /dev/null +++ b/lib/NGCP/Panel/Form/Administrator/APIDownDelete.pm @@ -0,0 +1,34 @@ +package NGCP::Panel::Form::Administrator::APIDownDelete; +use HTML::FormHandler::Moose; +use HTML::FormHandler::Widget::Block::Bootstrap; +use Moose::Util::TypeConstraints; +extends 'HTML::FormHandler'; + +has '+widget_wrapper' => (default => 'Bootstrap'); +sub build_render_list {[qw(actions)]} + +has_field 'key_actions' => ( + type => 'Compound', + do_label => 0, + do_wrapper => 1, + wrapper_class => [qw(row pull-right)], +); + + +has_field 'key_actions.download' => ( + type => 'Submit', + value => 'Download', + element_class => [qw(btn btn-primary)], + wrapper_class => [qw(pull-right)], +); + +has_field 'key_actions.delete' => ( + type => 'Submit', + value => 'Delete', + element_class => [qw(btn btn-secondary)], + wrapper_class => [qw(pull-right)], +); + +has_block 'actions' => (tag => 'div', class => [qw(modal-footer)], render_list => [qw(key_actions)],); + +1; diff --git a/lib/NGCP/Panel/Form/Administrator/APIGenerate.pm b/lib/NGCP/Panel/Form/Administrator/APIGenerate.pm new file mode 100644 index 0000000000..5baf4f07aa --- /dev/null +++ b/lib/NGCP/Panel/Form/Administrator/APIGenerate.pm @@ -0,0 +1,26 @@ +package NGCP::Panel::Form::Administrator::APIGenerate; +use HTML::FormHandler::Moose; +use HTML::FormHandler::Widget::Block::Bootstrap; +use Moose::Util::TypeConstraints; +extends 'HTML::FormHandler'; + +has '+widget_wrapper' => (default => 'Bootstrap'); +sub build_render_list {[qw(actions)]} + +has_field 'key_actions' => ( + type => 'Compound', + do_label => 0, + do_wrapper => 1, + wrapper_class => [qw(row pull-right)], +); + +has_field 'key_actions.generate' => ( + type => 'Submit', + value => 'Generate', + element_class => [qw(btn btn-primary)], + wrapper_class => [qw(pull-right)], +); + +has_block 'actions' => (tag => 'div', class => [qw(modal-footer)], render_list => [qw(key_actions)],); + +1; diff --git a/lib/NGCP/Panel/Model/CA.pm b/lib/NGCP/Panel/Model/CA.pm new file mode 100644 index 0000000000..2a41880dc7 --- /dev/null +++ b/lib/NGCP/Panel/Model/CA.pm @@ -0,0 +1,131 @@ +package NGCP::Panel::Model::CA; +use Sipwise::Base; +use MIME::Base64 qw(decode_base64); +use Path::Tiny qw(); +use Time::HiRes qw(); +use Types::Path::Tiny qw(AbsDir); +use Sys::Hostname qw(hostname); +extends 'Catalyst::Component'; + +has('ca_selfsign_template', is => 'ro', isa => 'Str', default => sub { <<'' }); +organization = "Sipwise GmbH" +unit = "Dept. of Issuing Snakeoil Certificates" +locality = "Brunn am Gebirge" +state = "Niederösterreich" +country = AT +cn = "*.sipwise.com" +expiration_days = 1000 +ca +cert_signing_key + +has('server_signingrequest_template', is => 'ro', isa => 'Str', default => sub { <<"" }); +cn = "@{[ hostname ]}" +expiration_days = 365 +tls_www_server +signing_key +encryption_key + +has('server_signing_template', is => 'ro', isa => 'Str', default => sub { <<'' }); +expiration_days = 365 +honor_crq_extensions + +sub client_signing_template { + my ($self, $serial) = @_; + return <<""; +cn = "Sipwise NGCP API client certificate" +expiration_days = 365 +serial = $serial +tls_www_client +signing_key +encryption_key + +} + +has('log', is => 'rw', isa => 'Log::Log4perl::Catalyst',); +has('prefix', is => 'ro', isa => AbsDir, coerce => 1, default => '/etc/ssl/ngcp/api'); + +sub COMPONENT { + my ($class, $app, $args) = @_; + $args = $class->merge_config_hashes($class->config, $args); + my $self = $class->new($app, $args); + no autobox::Core; # wonky initialisation order + $self->log($app->log); + return $self; +} + +sub make_ca { + my ($self) = @_; + my $command = sprintf 'certtool -p --bits 3248 --outfile %s 1>&- 2>&-', $self->prefix->child('ca-key.pem'); + $self->log->debug($command); + system $command; + my $ca_selfsign_template = Path::Tiny->tempfile; + $ca_selfsign_template->spew_utf8($self->ca_selfsign_template); + $command = sprintf 'certtool -s --load-privkey %s --outfile %s --template %s 1>&- 2>&-', + $self->prefix->child('ca-key.pem'), $self->prefix->child('ca-cert.pem'), $ca_selfsign_template->stringify; + $self->log->debug($command); + system $command; + return; +} + +sub make_server { + my ($self) = @_; + my $command = sprintf 'certtool -p --bits 3248 --outfile %s 1>&- 2>&-', $self->prefix->child('server-key.pem'); + $self->log->debug($command); + system $command; + my $server_signingrequest_template = Path::Tiny->tempfile; + $server_signingrequest_template->spew($self->server_signingrequest_template); + $command = sprintf 'certtool -q --load-privkey %s --outfile %s --template %s 1>&- 2>&-', + $self->prefix->child('server-key.pem'), $self->prefix->child('server-csr.pem'), + $server_signingrequest_template->stringify; + $self->log->debug($command); + system $command; + my $server_signing_template = Path::Tiny->tempfile; + $server_signing_template->spew($self->server_signing_template); + $command = sprintf 'certtool -c --load-request %s --outfile %s --load-ca-certificate %s --load-ca-privkey %s ' . + '--template %s 1>&- 2>&-', $self->prefix->child('server-csr.pem'), $self->prefix->child('server-cert.pem'), + $self->prefix->child('ca-cert.pem'), $self->prefix->child('ca-key.pem'), $server_signing_template->stringify; + $self->log->debug($command); + system $command; + return; +} + +sub make_client { + my ($self, $serial) = @_; + my $client_key = Path::Tiny->tempfile; + my $command = sprintf 'certtool -p --bits 3248 --outfile %s 1>&- 2>&-', $client_key->stringify; + $self->log->debug($command); + system $command; + my $client_signing_template = Path::Tiny->tempfile; + $client_signing_template->spew($self->client_signing_template($serial)); + my $client_cert = Path::Tiny->tempfile; + $command = sprintf 'certtool -c --load-privkey %s --outfile %s --load-ca-certificate %s --load-ca-privkey %s ' . + '--template %s 1>&- 2>&-', $client_key->stringify, $client_cert->stringify, $self->prefix->child('ca-cert.pem'), + $self->prefix->child('ca-key.pem'), $client_signing_template->stringify; + $self->log->debug($command); + system $command; + my $cert_file = $self->client_cert_file($serial); + $cert_file->spew($client_cert->slurp . $client_key->slurp =~ s/.*(?=-----BEGIN RSA PRIVATE KEY-----)//mrs); + return; +} + +sub client_cert_file { + my ($self, $serial) = @_; + return $self->prefix->child("NGCP-API-client-certificate-$serial.pem"); +} + +__END__ + +=encoding UTF-8 + +=head1 NAME + +NGCP::Panel::Model::CA - certificate management model + +=head1 DESCRIPTION + +=head2 Generating prerequisite root certificates + + perl -mNGCP::Panel::Model::CA -e' + NGCP::Panel::Model::CA->new->make_ca; + NGCP::Panel::Model::CA->new->make_server; + ' diff --git a/share/static/css/main.css b/share/static/css/main.css index a907e2cc54..588641e8a7 100644 --- a/share/static/css/main.css +++ b/share/static/css/main.css @@ -139,6 +139,9 @@ input.ngcp-destination-field { div.modal-footer div.control-group div.controls { margin-left: 5px; } +div.modal-footer div.control-group div.controls div.form-actions.pull-right { + margin-left: 5px; +} /* --------- Datatable (main one) diff --git a/share/templates/administrator/list.tt b/share/templates/administrator/list.tt index 7e3d003413..68ac9d84ce 100644 --- a/share/templates/administrator/list.tt +++ b/share/templates/administrator/list.tt @@ -17,13 +17,26 @@ helper.dt_buttons = [ { name = 'Edit', uri = "/administrator/'+full[\"id\"]+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' }, { name = 'Delete', uri = "/administrator/'+full[\"id\"]+'/delete", class = 'btn-small btn-secondary', icon = 'icon-trash' }, + { name = 'API key', uri = "/administrator/'+full[\"id\"]+'/api_key", class = 'btn-small btn-info', icon = 'icon-lock' }, ]; helper.top_buttons = [ { name = 'Create Administrator', uri = c.uri_for('/administrator/create'), icon = 'icon-star' }, ]; + ELSE; + helper.dt_buttons = [ + { name = 'API key', uri = "/administrator/'+full[\"id\"]+'/api_key", class = 'btn-small btn-info', icon = 'icon-lock' }, + ]; END; PROCESS 'helpers/datatables.tt'; + + IF api_modal_flag; + PROCESS "helpers/modal.tt"; + modal_header(m.name = "API key"); + helper.form_object.render; + modal_footer(); + modal_script(m.close_target = helper.close_target); + END; -%] [% # vim: set tabstop=4 syntax=html expandtab: -%]