Commit 26015ada authored by Alain Takoudjou's avatar Alain Takoudjou Committed by Vincent Pelletier

initial implementation of certificate authority

The certificate authority is used to generate and sign certificate, there is 3 parts:
- web: which contains API to submit certificate signature request and to download signed certificate
- cliweb: which is a command line tool used to quickly generate private key and send certificate signature request, he will
also downlaod automatically the signed certificate as well as ca certificate.
- cli: is used to garbage collect certificate authority, all expired certificate, csr, crl and revocation will be trashed using this tool.

The first csr can be automatically signed, the rest will be signed by the adminitrator, first connection to /admin/ will ask to set password
the admin can see all csr (pending) then sign them. As soon as csr is signed, the client will download (cliweb) the certificate.

client can also renew or revoke his certificate using CA API. Renew and revoke are immediate, there is no admin approval.

on server side, the storage storage.py use sqlite to store all informations (certificat, csr, crl and revocations), there is no use of openssl here.
ca.py will invoke the storage to store or to get certificates.

the client store certificate directly on filesystem, so it can be read by apache, nginx, etc.
parent d2f8ede9
...@@ -15,3 +15,11 @@ ...@@ -15,3 +15,11 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>. # along with caucase. If not, see <http://www.gnu.org/licenses/>.
# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
This diff is collapsed.
...@@ -15,3 +15,35 @@ ...@@ -15,3 +15,35 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>. # along with caucase. If not, see <http://www.gnu.org/licenses/>.
import os
import argparse
from caucase.web import app, db
def parseArguments():
"""
Parse arguments for Certificate Authority cli instance.
"""
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--db-file', required=True,
help='Certificate authority data base file path')
return parser
def housekeeping(config):
"""
Start Storage housekeep method to cleanup garbages
"""
app.config.update(
DEBUG=False,
CSRF_ENABLED=True,
TESTING=False,
SQLALCHEMY_DATABASE_URI='sqlite:///%s' % config.db_file
)
from caucase.storage import Storage
storage = Storage(db)
storage.housekeep()
def main():
parser = parseArguments()
config = parser.parse_args()
housekeeping(config)
\ No newline at end of file
This diff is collapsed.
...@@ -33,3 +33,19 @@ class Found(CertificateAuthorityException): ...@@ -33,3 +33,19 @@ class Found(CertificateAuthorityException):
class BadSignature(CertificateAuthorityException): class BadSignature(CertificateAuthorityException):
"""Non-x509 signature check failed""" """Non-x509 signature check failed"""
class BadCertificateSigningRequest(CertificateAuthorityException):
"""CSR content doesn't contain all required elements"""
pass
class BadCertificate(CertificateAuthorityException):
"""Certificate is not a valid PEM content"""
pass
class CertificateVerificationError(CertificateAuthorityException):
"""Certificate is not valid, it was not signed by CA"""
pass
class ExpiredCertificate(CertificateAuthorityException):
"""Certificate has expired and could not be used"""
pass
\ No newline at end of file
# This file is part of caucase
# Copyright (C) 2017 Nexedi
# Alain Takoudjou <alain.takoudjou@nexedi.com>
# Vincent Pelletier <vincent@nexedi.com>
#
# caucase 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.
#
# caucase 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 caucase. If not, see <http://www.gnu.org/licenses/>.
.ui-table th, .ui-table td {
line-height: 1.5em;
text-align: left;
padding: .4em .5em;
vertical-align: middle;
}
.ui-overlay-a, .ui-page-theme-a, .ui-page-theme-a .ui-panel-wrapper {
background-color: #fff !important;
}
table .ui-table th, table .ui-table td {
vertical-align: middle;
}
.noshadow buton, .noshadow a, .noshadow input, .noshadow select {
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
}
.ui-error, html .ui-content .ui-error a, .ui-content a.ui-error {
color: red;
font-weight: bold;
}
html body {
overflow-x: hidden;
background: #fbfbfb;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
/* Toggle Styles */
#wrapper {
padding-left: 0;
-webkit-transition: all 0.6s ease;
-moz-transition: all 0.6s ease;
-o-transition: all 0.6s ease;
transition: all 0.6s ease;
}
#wrapper.toggled {
padding-left: 200px;
}
#sidebar-wrapper {
z-index: 1000;
position: fixed;
left: 250px;
width: 0;
height: 100%;
margin-left: -250px;
overflow-y: auto;
background-color:#312A25 !Important;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
#wrapper.toggled #sidebar-wrapper {
width: 0;
}
#page-content-wrapper {
width: 100%;
position: absolute;
padding: 10px;
}
#wrapper.toggled #page-content-wrapper {
position: absolute;
margin-left:-250px;
}
/* Sidebar Styles */
.nav-side-menu {
overflow: auto;
font-family: verdana;
font-size: 12px;
font-weight: 200;
background-color: #2a2f35; /* #313130 */
position: fixed;
top: 0px;
width: 300px;
height: 100%;
color: #e1ffff;
}
.nav-side-menu .brand {
background-color: #404040;
line-height: 50px;
display: block;
text-align: center;
font-size: 14px;
}
.nav-side-menu .toggle-btn {
display: none;
}
.nav-side-menu ul,
.nav-side-menu li {
list-style: none;
padding: 0px;
margin: 0px;
line-height: 35px;
cursor: pointer;
/*
.collapsed{
.arrow:before{
font-family: FontAwesome;
content: "\f053";
display: inline-block;
padding-left:10px;
padding-right: 10px;
vertical-align: middle;
float:right;
}
}
*/
}
.nav-side-menu ul :not(collapsed) .arrow:before,
.nav-side-menu li :not(collapsed) .arrow:before {
font-family: FontAwesome;
content: "\f078";
display: inline-block;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
float: right;
}
.nav-side-menu ul .active,
.nav-side-menu li .active {
border-left: 3px solid #d19b3d;
background-color: #4f5b69;
}
.nav-side-menu ul .sub-menu li.active,
.nav-side-menu li .sub-menu li.active {
color: #d19b3d;
}
.nav-side-menu ul .sub-menu li.active a,
.nav-side-menu li .sub-menu li.active a {
color: #d19b3d;
}
.nav-side-menu ul .sub-menu li,
.nav-side-menu li .sub-menu li {
background-color: #181c20;
border: none;
line-height: 28px;
border-bottom: 1px solid #23282e;
margin-left: 0px;
}
.nav-side-menu ul .sub-menu li:hover,
.nav-side-menu li .sub-menu li:hover {
background-color: #020203;
}
.nav-side-menu ul .sub-menu li:before,
.nav-side-menu li .sub-menu li:before {
font-family: FontAwesome;
content: "\f105";
display: inline-block;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
}
.nav-side-menu li {
padding-left: 0px;
border-left: 3px solid #2e353d;
border-bottom: 1px solid #23282e;
}
.nav-side-menu li a {
text-decoration: none;
color: #e1ffff;
display: block;
}
.nav-side-menu li a i {
padding-left: 10px;
width: 20px;
padding-right: 25px;
font-size: 18px;
}
.nav-side-menu li:hover {
border-left: 3px solid #d19b3d;
background-color: #4f5b69;
-webkit-transition: all 1s ease;
-moz-transition: all 1s ease;
-o-transition: all 1s ease;
-ms-transition: all 1s ease;
transition: all 1s ease;
}
@media (max-width: 767px) {
.nav-side-menu {
position: relative;
width: 100%;
margin-bottom: 10px;
}
.nav-side-menu .toggle-btn {
display: block;
cursor: pointer;
position: absolute;
right: 10px;
top: 10px;
z-index: 10 !important;
padding: 3px;
background-color: #ffffff;
color: #000;
width: 40px;
text-align: center;
}
.brand {
text-align: left !important;
font-size: 22px;
padding-left: 20px;
line-height: 50px !important;
}
}
@media (min-width: 767px) {
.nav-side-menu .menu-list .menu-content {
display: block;
}
#main {
width:calc(100% - 300px);
float: right;
}
}
body {
margin: 0px;
padding: 0px;
}
pre {
max-height: 600px;
}
.col-centered {
float: none;
margin: 0 auto;
}
.clickable{
cursor: pointer;
}
.table .panel-heading div {
margin-top: -18px;
font-size: 15px;
}
.table .panel-heading div span{
margin-left:5px;
}
.table .panel-body{
display: none;
}
.container .table>tbody>tr>td, .table>tbody>tr>th, .table>tfoot>tr>td, .table>tfoot>tr>th, .table>thead>tr>td, .table>thead>tr>th {
vertical-align: middle;
}
.margin-top-40 {
margin-top:40px;
}
.margin-lr-20 {
margin: 0 20px;
}
.flashes-messages div:first-child {
margin-top: 30px;
}
/* Dashboard boxes */
.dash-panel {
text-align: center;
padding: 1px 0;
}
html body a:hover > .dash-panel h4 {
text-decoration: none;
}
.dash-panel:hover {
background-color: #e6e6e6;
border-color: #adadad;
cursor: pointer;
}
.dash {
position: relative;
text-align: center;
width: 120px;
height: 55px;
margin: 10px auto 10px auto;
}
#dash-blue .number {
color: #30a5ff;
}
#dash-orange .number {
color: #ffb53e;
}
#dash-teal .number {
color: #1ebfae;
}
#dash-red .number {
color: #ef4040;
}
#dash-darkred .number {
color: #bd0849;
}
.dash .number {
display: block;
position: absolute;
font-size: 46px;
width: 120px;
}
.alert-error {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
$( document ).ready(function() {
$("#menu-toggle").click(function(e) {
e.preventDefault();
$("#wrapper").toggleClass("toggled");
});
});
(function(){
'use strict';
var $ = jQuery;
$.fn.extend({
filterTable: function(){
return this.each(function(){
$(this).on('keyup', function(e){
$('.filterTable_no_results').remove();
var $this = $(this),
search = $this.val().toLowerCase(),
target = $this.attr('data-filters'),
$target = $(target),
$rows = $target.find('tbody tr');
if(search === '') {
$rows.show();
} else {
$rows.each(function(){
var $this = $(this);
$this.text().toLowerCase().indexOf(search) === -1 ? $this.hide() : $this.show();
});
if($target.find('tbody tr:visible').size() === 0) {
var col_count = $target.find('tr').first().find('td').size();
var no_results = $('<tr class="filterTable_no_results"><td colspan="'+col_count+'">No results found</td></tr>');
$target.find('tbody').append(no_results);
}
}
});
});
}
});
$('[data-action="filter"]').filterTable();
})(jQuery);
$(function(){
// attach table filter plugin to inputs
$('[data-action="filter"]').filterTable();
$('.container').on('click', '.panel-heading span.filter', function(e){
var $this = $(this),
$panel = $this.parents('.panel');
$panel.find('.panel-body').slideToggle();
if($this.css('display') != 'none') {
$panel.find('.panel-body input').focus();
}
});
$('[data-toggle="tooltip"]').tooltip();
});
\ No newline at end of file
This diff is collapsed.
{% extends "layout.html" %}
{% block content %}
<form class="form-signin" method="POST" action="/admin/setpassword">
<h2 class="form-signin-heading" style="margin-bottom: 30px">Set admin password</h2>
<label for="pw" class="sr-only">Password</label>
<input type="inputPassword" name="password" id="pw" class="form-control" placeholder="password" required autofocus>
<label for="pw2" class="sr-only">Confirm Password:</label>
<input type="inputPassword" name="password2" id="pw2" class="form-control" placeholder="Confirm password" required autofocus>
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">configure</button>
</form>
{% endblock %}
{% extends "layout.html" %}
{% block pre_content %}
<div class="row">
<div class="col-sm-7 col-md-6 col-lg-5 col-centered">
{% endblock %}
{% block post_content %}
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends 'flask_user/flask_user_base.html' %}
\ No newline at end of file
{% extends 'flask_user/flask_user_base.html' %}
\ No newline at end of file
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type=application/javascript src="{{ url_for('static', filename='scripts/index.js') }}"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Certificate Authority web</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/styles.css') }}">
<link href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Certificate authority<small> Signed Certificates</small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-success margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">List of signed Certificates</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Common Name</th>
<th>Signature Date</th>
<th>Expiration Date</th>
<th></th>
</tr>
</thead>
<tbody>
{% for cert in data_list -%}
<tr>
<td>{{ cert['index'] }}</td>
<td><a href="/crt/{{ cert['crt_id'] }}">{{ cert['crt_id'] }}</a></td>
<td>{{ cert['common_name'] }}</td>
<td>{{ cert['start_before'] }}</td>
<td>{{ cert['expire_after'] }}</td>
<td><a class="btn btn-default" href="/crt/{{ cert['crt_id'] }}" role="button" title="Download file"><i class="fa fa-download" aria-hidden="true"></i></a></td>
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
{% include "head.html" %}
</head>
<body>
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="nav-side-menu">
<div class="brand">Certificate Authority</div>
<i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i>
<div class="menu-list">
<ul id="menu-content" class="menu-content collapse out">
{% if not session.user_id %}
<li>
<a href="/">
<i class="fa fa-home" aria-hidden="true"></i> Public Home
</a>
</li>
<li>
<a href="/user/sign-in">
<i class="fa fa-cog" aria-hidden="true"></i> Manage Certificates
</a>
</li>
{% else -%}
<li>
<a href="/admin/">
<i class="fa fa-home" aria-hidden="true"></i> Signed Certificates
</a>
</li>
<li><a href="/admin/csr_requests"><i class="fa fa-tachometer" aria-hidden="true"></i> Manage CSR
<span style="font-weight: bold; margin-left: 5px; margin-right: 5px; color: #56d8ce;">[ {{ session.count_csr }} ] </span></a></li>
<!--<li><a href="/signed_certs"><i class="fa fa-check-square" aria-hidden="true"></i> Signed Certificates</a></li>
<li><a href="/revoked_certs"><i class="fa fa-minus-square" aria-hidden="true"></i> Revoked Certificates</a></li>-->
<li><a href="/admin/profile"><i class="fa fa-user" aria-hidden="true"></i> User Profile</a></li>
<!--<li><a href="/admin/logs"><i class="fa fa-book" aria-hidden="true"></i> Certificate Authority Logs</a></li>-->
<li><a href="/admin/logout"><i class="fa fa-sign-out" aria-hidden="true"></i> Logout</a></li>
{% endif -%}
</ul>
</div>
</div>
<div class="container" id="main">
<div class="flashes-messages">
{% with messages = get_flashed_messages(with_categories=true) %}
<!-- Categories: success (green), info (blue), warning (yellow), danger (red) -->
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ message|safe }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% block pre_content %}{% endblock %}
{% block content %}{% endblock %}
{% block post_content %}{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% include "footer.html" %}
</body>
</html>
\ No newline at end of file
{% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None, readonly=false) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% endif %}
{{ field(class_='form-control', readonly=readonly, **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{%- endmacro %}
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox">
<label>
{{ field(type='checkbox', **kwargs) }} {{ label }}
</label>
</div>
{%- endmacro %}
{% macro render_radio_field(field) -%}
{% for value, label, checked in field.iter_choices() %}
<div class="radio">
<label>
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
{{ label }}
</label>
</div>
{% endfor %}
{%- endmacro %}
{% macro render_submit_field(field, label=None, tabindex=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
{#<button type="submit" class="form-control btn btn-lg btn-primary btn-block">{{label}}</button>#}
<input type="submit" class="btn btn-lg btn-primary btn-block" value="{{label}}"
{% if tabindex %}tabindex="{{ tabindex }}"{% endif %}
>
{%- endmacro %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Administration <small>Certificate authority web</small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-primary margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">Certificates Signature request list</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th class="hidden-xs">#</th>
<th class="hidden-xs">CSR ID</th>
<th class="hidden-xs">CRT ID</th>
<th>Common Name</th>
<th class="hidden-xs">Request Date</th>
<th></th>
</tr>
</thead>
<tbody>
{% for csr in data_list -%}
<tr>
<td class="hidden-xs">{{ csr['index'] }}</td>
<td class="hidden-xs">{{ csr['csr_id'] }}</td>
<td class="hidden-xs">{{ csr['crt_id'] }}</td>
<td>{{ csr['common_name'] }}</td>
<td class="hidden-xs">{{ csr['creation_date'] }}</td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Action <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="/admin/signcert?csr_id={{ csr['csr_id'] }}"><i class="fa fa-check-square" aria-hidden="true"></i> Sign</a></li>
<li><a href="/csr/{{ csr['csr_id'] }}"><i class="fa fa-eye" aria-hidden="true"></i> Download</a></li>
<li role="separator" class="divider"></li>
<li><a href="/admin/deletecsr?csr_id={{ csr['csr_id'] }}"><i class="fa fa-times" aria-hidden="true"></i> Delete</a></li>
</ul>
</div>
</td>
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>User account information</h1>
</div>
<div style="padding: 15px">
<p>
<a class="btn btn-default" href="{{ url_for('user.change_password') }}" role="button" title="Download file">
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> Change password</a>
</p>
{% from "macros.html" import render_field, render_submit_field %}
<form action="" method="POST" class="form" role="form">
<div class="row">
<div class="col-sm-6 col-md-5 col-lg-4">
{{ form.hidden_tag() }}
{{ render_field(form.username, tabindex=230, readonly=true) }}
{{ render_field(form.first_name, tabindex=240) }}
{{ render_field(form.last_name, tabindex=250) }}
{{ render_field(form.email, tabindex=260) }}
{{ render_submit_field(form.submit, tabindex=280) }}
</div>
</div>
</form>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>View {{ cert_type }} Details <small></small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ name |safe }}</h3>
</div>
<div class="panel-body">
<pre>{{ content }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Certificate Authority <small>view logs content</small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ filename }}</h3>
</div>
<div class="panel-body">
<p>
<a class="btn btn-default" href="/admin/view_logs?full=true" role="button" title="Download file">
<i class="fa fa-file-text" aria-hidden="true"></i> Full log</a>
<a class="btn btn-default" href="/admin/view_logs" role="button" title="Download file">
<i class="fa fa-file-text" aria-hidden="true"></i> Tail log</a>
</p>
<pre style="max-height: 600px;">{{ content }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
# This file is part of caucase
# Copyright (C) 2017 Nexedi
# Alain Takoudjou <alain.takoudjou@nexedi.com>
# Vincent Pelletier <vincent@nexedi.com>
#
# caucase 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.
#
# caucase 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 caucase. If not, see <http://www.gnu.org/licenses/>.
import json
from caucase.exceptions import BadSignature, CertificateVerificationError
from OpenSSL import crypto, SSL
from pyasn1.codec.der import encoder as der_encoder
from pyasn1.type import tag
from pyasn1_modules import rfc2459
class GeneralNames(rfc2459.GeneralNames):
"""
rfc2459 has wrong tagset.
"""
tagSet = tag.TagSet(
(),
tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0),
)
class DistributionPointName(rfc2459.DistributionPointName):
"""
rfc2459 has wrong tagset.
"""
tagSet = tag.TagSet(
(),
tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0),
)
class ExtensionType():
CRL_DIST_POINTS = "crlDistributionPoints"
BASIC_CONSTRAINTS = "basicConstraints"
KEY_USAGE = "keyUsage"
NS_CERT_TYPE = "nsCertType"
NS_COMMENT = "nsComment"
SUBJECT_KEY_ID = "subjectKeyIdentifier"
AUTH_KEY_ID = "authorityKeyIdentifier"
class X509Extension(object):
known_extension_list = [name for (attr, name) in vars(ExtensionType).items()
if attr.isupper()]
def setX509Extension(self, ext_type, critical, value, subject=None, issuer=None):
if not ext_type in self.known_extension_list:
raise ValueError('Extension type is not known from ExtensionType class')
if ext_type == ExtensionType.CRL_DIST_POINTS:
cdp = self._getCrlDistPointExt(value)
return crypto.X509Extension(
b'%s' % ext_type,
critical,
'DER:' + cdp.encode('hex'),
subject=subject,
issuer=issuer,
)
else:
return crypto.X509Extension(
ext_type,
critical,
value,
subject=subject,
issuer=issuer,
)
def _getCrlDistPointExt(self, cdp_list):
cdp = rfc2459.CRLDistPointsSyntax()
position = 0
for cdp_type, cdp_value in cdp_list:
cdp_entry = rfc2459.DistributionPoint()
general_name = rfc2459.GeneralName()
if not cdp_type in ['dNSName', 'directoryName', 'uniformResourceIdentifier']:
raise ValueError("crlDistributionPoints GeneralName '%s' is not valid" % cdp_type)
general_name.setComponentByName(cdp_type, cdp_value)
general_names = GeneralNames()
general_names.setComponentByPosition(0, general_name)
name = DistributionPointName()
name.setComponentByName('fullName', general_names)
cdp_entry.setComponentByName('distributionPoint', name)
cdp.setComponentByPosition(position, cdp_entry)
position += 1
return der_encoder.encode(cdp)
def setCaExtensions(self, cert_obj):
"""
extensions for default certificate
"""
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.BASIC_CONSTRAINTS, True,
"CA:TRUE, pathlen:0"),
self.setX509Extension(ExtensionType.NS_COMMENT,
False, "OpenSSL CA Certificate"),
self.setX509Extension(ExtensionType.KEY_USAGE,
True, "keyCertSign, cRLSign"),
self.setX509Extension(ExtensionType.SUBJECT_KEY_ID,
False, "hash", subject=cert_obj),
])
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.AUTH_KEY_ID,
False, "keyid:always,issuer", issuer=cert_obj)
])
def setDefaultExtensions(self, cert_obj, subject=None, issuer=None, crl_url=None):
"""
extensions for default certificate
"""
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.BASIC_CONSTRAINTS, False, "CA:FALSE"),
self.setX509Extension(ExtensionType.NS_COMMENT,
False, "OpenSSL Generated Certificate"),
self.setX509Extension(ExtensionType.SUBJECT_KEY_ID,
False, "hash", subject=subject),
])
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.AUTH_KEY_ID,
False, "keyid,issuer", issuer=issuer)
])
if crl_url:
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.CRL_DIST_POINTS,
False, [("uniformResourceIdentifier", crl_url)])
])
def setDefaultCsrExtensions(self, cert_obj, subject=None, issuer=None):
"""
extensions for certificate signature request
"""
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.BASIC_CONSTRAINTS, False, "CA:FALSE"),
self.setX509Extension(ExtensionType.KEY_USAGE,
False, "nonRepudiation, digitalSignature, keyEncipherment"),
])
def getSerialToInt(x509):
return '{0:x}'.format(int(x509.get_serial_number()))
def validateCertAndKey(cert_pem, key_pem):
ctx = SSL.Context(SSL.TLSv1_METHOD)
ctx.use_privatekey(key_pem)
ctx.use_certificate(cert_pem)
try:
ctx.check_privatekey()
except SSL.Error:
return False
else:
return True
def verifyCertificateChain(cert_pem, trusted_cert_list, crl=None):
# Create and fill a X509Sore with trusted certs
store = crypto.X509Store()
for trusted_cert in trusted_cert_list:
store.add_cert(trusted_cert)
if crl:
store.add_crl(crl)
store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
store_ctx = crypto.X509StoreContext(store, cert_pem)
# Returns None if certificate can be validated
try:
result = store_ctx.verify_certificate()
except crypto.X509StoreContextError, e:
raise CertificateVerificationError('Certificate verification error: %s' % str(e))
except crypto.Error, e:
raise CertificateVerificationError('Certificate verification error: %s' % str(e))
if result is None:
return True
else:
return False
def checkCertificateValidity(ca_cert_list, cert_pem, key_pem=None):
if not verifyCertificateChain(cert_pem, ca_cert_list):
return False
if key_pem:
return validateCertAndKey(cert_pem, key_pem)
return True
def sign(data, key, digest="sha256"):
"""
Sign a data using digest and return signature.
"""
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
sign = crypto.sign(pkey, data, digest)
#data_base64 = base64.b64encode(sign)
return sign
def verify(data, cert_string, signature, digest="sha256"):
"""
Verify the signature for a data string.
cert_string: is the certificate content as string
signature: is generate using 'signData' from the data to verify
data: content to verify
digest: by default is sha256, set the correct value
"""
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_string)
return crypto.verify(x509, signature, data, digest.encode("ascii", 'ignore'))
def wrap(payload, key, digest_list):
"""
Sign payload (json-serialised) with key, using one of the given digests.
"""
# Choose a digest between the ones supported
# how to choose the default digest ?
digest = digest_list[0]
payload = json.dumps(payload)
return {
"payload": payload,
"digest": digest,
"signature": sign(payload + digest + ' ', key, digest).encode('base64'),
}
def unwrap(wrapped, getCertificate, digest_list):
"""
Raise if signature does not match payload. Returns payload.
"""
# Check whether given digest is allowed
if wrapped['digest'] not in digest_list:
raise BadSignature('Given digest is not supported')
payload = json.loads(wrapped['payload'])
crt = getCertificate(payload)
try:
verify(wrapped['payload'] + wrapped['digest'] + ' ', crt, wrapped['signature'].decode('base64'), wrapped['digest'])
except crypto.Error, e:
raise BadSignature('Signature mismatch: %s' % str(e))
return payload
def tail(path, lines=100):
"""
Returns the last `lines` lines of file `path`.
"""
f = open(path).read()
BUFSIZ = 1024
f.seek(0, 2)
bytes = f.tell()
size = lines + 1
block = -1
data = []
while size > 0 and bytes > 0:
if bytes - BUFSIZ > 0:
f.seek(block * BUFSIZ, 2)
data.insert(0, f.read(BUFSIZ))
else:
f.seek(0, 0)
data.insert(0, f.read(bytes))
linesFound = data[0].count('\n')
size -= linesFound
bytes -= BUFSIZ
block -= 1
return '\n'.join(''.join(data).splitlines()[-lines:])
This diff is collapsed.
...@@ -15,30 +15,41 @@ ...@@ -15,30 +15,41 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>. # along with caucase. If not, see <http://www.gnu.org/licenses/>.
import json
from .exceptions import BadSignature
def wrap(payload, key, digest_list): import os
""" from caucase.web import parseArguments, configure_flask, app
Sign payload (json-serialised) with key, using one of the given digests. from werkzeug.contrib.fixers import ProxyFix
"""
# Choose a digest between the ones supported def readConfigFromFile(config_file):
payload = json.dumps(payload) config_list = []
return { with open(config_file) as f:
"payload": payload, for line in f.readlines():
"digest": digest, if not line or line.startswith('#'):
"signature": sign(payload + digest + ' ', key, digest).encode('base64'), continue
} line_list = line.strip().split(' ')
if len(line_list) == 1:
def unwrap(wrapped, getCertificate, digest_list): config_list.append('--%s' % line_list[0].strip())
elif len(line_list) > 1:
config_list.append('--%s' % line_list[0].strip())
config_list.append(' '.join(line_list[1:]))
return parseArguments(config_list)
def start_wsgi():
""" """
Raise if signature does not match payload. Returns payload. Start entry for wsgi, do not run app.run, read config from file
""" """
# Check whether given digest is allowed if os.environ.has_key('CA_CONFIGURATION_FILE'):
raise BadSignature('Given digest is not supported') config_file = os.environ['CA_CONFIGURATION_FILE']
payload = json.loads(wrapped['payload']) else:
crt = getCertificate(payload) config_file = 'ca.conf'
if not check(wrapped['payload'] + wrapped['digest'] + ' ', crt, wrapped['signature'].decode('base64'), wrapped['digest']):
raise BadSignature('Signature mismatch') configure_flask(readConfigFromFile(config_file))
return payload
app.wsgi_app = ProxyFix(app.wsgi_app)
app.logger.info("Certificate Authority server ready...")
if __name__ == 'caucase.wsgi':
start_wsgi()
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment