add more pki features, bug fixes, try not to use jquery

This commit is contained in:
TuxCoder 2020-05-25 20:23:27 +02:00
parent 38932aef44
commit 6c388c8129
18 changed files with 675 additions and 1068 deletions

View file

@ -16,7 +16,7 @@ LDAP_BIND_PW = '123456'
PKI_PATH = f'{DATA_FOLDER}/pki' PKI_PATH = f'{DATA_FOLDER}/pki'
DOMAIN = 'example.com' DOMAIN = 'example.com'
SERVER_NAME = f'account.{ DOMAIN }:9090' #SERVER_NAME = f'account.{ DOMAIN }:9090'
HYDRA_REQUEST_TIMEOUT_SECONDS = 3 HYDRA_REQUEST_TIMEOUT_SECONDS = 3
HYDRA_ADMIN_URL = 'http://127.0.0.1:4445' HYDRA_ADMIN_URL = 'http://127.0.0.1:4445'

View file

@ -0,0 +1,45 @@
"use strict";
const $ = document.querySelector.bind(document)
const _ = document.getElementById
export class ConfirmDialog {
constructor(message) {
this._div = document.getElementById('confirm-dialog-template').content.querySelector('div').cloneNode(true);
this._div.querySelector('.modal-body').innerHTML = message;
}
show() {
var self = this;
this._promise = new Promise((resolve, reject) => {
self._resolve = resolve;
self._reject = reject;
});
this._div.querySelectorAll('.close').forEach(function (o){
o.onclick=self.cancel.bind(self);
});
this._div.querySelector('.process').onclick = () => {
self._close();
self._resolve();
};
$('.messages-box').appendChild(this._div);
return this._promise
}
cancel() {
this._close()
this._reject('canceled by user');
}
_close() {
$('.messages-box').removeChild(this._div);
}
}

View file

@ -1,14 +1,20 @@
import 'jquery'; import 'jquery';
import 'bootstrap'; import 'bootstrap';
import 'jquery-form' import 'jquery-form'
import {ConfirmDialog} from './confirm-modal.js';
window.$ =window.jQuery = require('jquery'); jQuery = window.$ = window.jQuery = require('jquery');
var forge = require('node-forge'); var forge = require('node-forge');
var QRCode = require("qrcode-svg"); var QRCode = require("qrcode-svg");
var pki = require('node-forge/lib/pki'); var pki = require('node-forge/lib/pki');
var asn1 = require('node-forge/lib/asn1'); var asn1 = require('node-forge/lib/asn1');
var pkcs12 = require('node-forge/lib/pkcs12'); var pkcs12 = require('node-forge/lib/pkcs12');
var util = require('node-forge/lib/util'); var util = require('node-forge/lib/util');
import SimpleFormSubmit from "simple-form-submit";
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
/* /*
Convert an ArrayBuffer into a string Convert an ArrayBuffer into a string
@ -32,12 +38,13 @@ function randBase32() {
return result; return result;
} }
window.$(document).ready(function () { window.ConfirmDialog = ConfirmDialog;
$('#sidebarCollapse').on('click', function () {
$('nav.sidebar').toggleClass('d-none');
});
});
window.$(document).ready(function () {
$('#sidebarCollapse').onclick = function () {
$('nav.sidebar').classList.toggle('d-none');
};
});
window.totp = { window.totp = {
init_list: function(){ init_list: function(){
@ -46,18 +53,18 @@ window.totp = {
//create new TOTP secret, create qrcode and ask for token. //create new TOTP secret, create qrcode and ask for token.
var form = $('form'); var form = $('form');
var secret = randBase32(); var secret = randBase32();
var input_secret = form.find('#secret') var input_secret = form.querySelector('#secret')
if(input_secret.val() == '') { if(input_secret.value == '') {
input_secret.val(secret); input_secret.value = secret;
} }
form.find('#name').on('change',window.totp.generate_qrcode); form.querySelector('#name').on('change',window.totp.generate_qrcode);
window.totp.generate_qrcode(); window.totp.generate_qrcode();
}, },
generate_qrcode: function(){ generate_qrcode: function(){
var form = $('form'); var form = $('form');
var secret = form.find('#secret').val(); var secret = form.querySelector('#secret').value;
var name = form.find('#name').val(); var name = form.querySelector('#name').value;
var issuer = 'Lenticular%20Cloud'; var issuer = 'Lenticular%20Cloud';
var svg_container = $('#svg-container') var svg_container = $('#svg-container')
var svg = new QRCode(`otpauth://totp/${issuer}:${name}?secret=${secret}&issuer=${issuer}`).svg(); var svg = new QRCode(`otpauth://totp/${issuer}:${name}?secret=${secret}&issuer=${issuer}`).svg();
@ -84,48 +91,52 @@ window.client_cert = {
}, },
generate_private_key: function() { generate_private_key: function() {
var form = $('form#gen-key-form'); var form = $('form#gen-key-form');
var key_size = form.find('#key-size').val(); var key_size = form.querySelector('#key-size').value;
var valid_time = form.find('input[name=valid_time]').val(); var valid_time = form.querySelector('input[name=valid_time]').value;
$('button#generate-key')[0].style['display'] = 'none'; $('button#generate-key').style['display'] = 'none';
pki.rsa.generateKeyPair({bits: key_size, workers: 2}, function(err, keypair) { pki.rsa.generateKeyPair({bits: key_size, workers: 2}, function(err, keypair) {
console.log(keypair); console.log(keypair);
form.data('keypair', keypair);
//returns the exported key to a hidden form //returns the exported key to a hidden form
var form_sign_key = $('#gen-key-sign form'); var form_sign_key = $('#gen-key-sign form');
form_sign_key.find('textarea[name=publickey]').val(pki.publicKeyToPem(keypair.publicKey)); form_sign_key.querySelector('textarea[name=publickey]').value = pki.publicKeyToPem(keypair.publicKey);
form_sign_key.find('input[name=valid_time]').val(valid_time); form_sign_key.querySelector('input[name=valid_time]').value = valid_time;
form_sign_key.ajaxForm({ SimpleFormSubmit.submitForm(form_sign_key.action, form_sign_key)
success: function(response) { .then(response => {
// get certificate response.json().then( response => {
var data = response['data']; // get certificate
var data = response.data;
var certs = [ var certs = [
pki.certificateFromPem(data.cert), pki.certificateFromPem(data.cert),
pki.certificateFromPem(data.ca_cert) pki.certificateFromPem(data.ca_cert)
]; ];
var password = form.find('#cert-password').val(); var password = form.querySelector('#cert-password').value;
var keypair = form.data('keypair'); var p12Asn1;
var p12Asn1; if (password == '') {
if (password == '') { p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, null, {algorithm: '3des'}); // without password
p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, null, {algorithm: '3des'}); // without password } else {
} else { p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, password, {algorithm: '3des'}); // without password
p12Asn1 = pkcs12.toPkcs12Asn1(keypair.privateKey, certs, password, {algorithm: '3des'}); // without password }
} var p12Der = asn1.toDer(p12Asn1).getBytes();
var p12Der = asn1.toDer(p12Asn1).getBytes(); var p12b64 = util.encode64(p12Der);
var p12b64 = util.encode64(p12Der);
var button = $('#save-button')[0]; var button = $('#save-button');
button.href= "data:application/x-pkcs12;base64," + p12b64 button.href= "data:application/x-pkcs12;base64," + p12b64
button.style['display'] ='block'; button.style['display'] ='block';
} });
});
});
// submit hidden form
form_sign_key.submit();
}); });
},
revoke_certificate: function(href, id){
var dialog = new ConfirmDialog(`Are you sure to revoke the certificate with the fingerprint ${id}?`);
dialog.show().then(()=>{
fetch(href, {
method: 'DELETE'
});
});
return false;
} }
}; };

View file

@ -3,3 +3,13 @@
@import "~@fortawesome/fontawesome-free/css/all.css"; @import "~@fortawesome/fontawesome-free/css/all.css";
//@import "~datatables.net-bs4/css/dataTables.bootstrap4.css"; //@import "~datatables.net-bs4/css/dataTables.bootstrap4.css";
.messages-box {
position: fixed;
width: 100%;
margin-top: 30px;
z-index: 500;
}

View file

@ -54,11 +54,12 @@ def init_app(name=None):
hydra_client = hydra.ApiClient(hydra_config) hydra_client = hydra.ApiClient(hydra_config)
app.hydra_api = hydra.AdminApi(hydra_client) app.hydra_api = hydra.AdminApi(hydra_client)
from .views import auth_views, frontend_views, init_login_manager, api_views from .views import auth_views, frontend_views, init_login_manager, api_views, pki_views
init_login_manager(app) init_login_manager(app)
app.register_blueprint(auth_views) app.register_blueprint(auth_views)
app.register_blueprint(frontend_views) app.register_blueprint(frontend_views)
app.register_blueprint(api_views) app.register_blueprint(api_views)
app.register_blueprint(pki_views)
@app.before_request @app.before_request
def befor_request(): def befor_request():

View file

@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from collections.abc import MutableSequence from collections.abc import MutableSequence
from datetime import datetime from datetime import datetime
from dateutil import tz
import pyotp import pyotp
import json import json
@ -137,10 +138,13 @@ class Service(object):
class Certificate(object): class Certificate(object):
def __init__(self, cn, ca_name, cert_data): def __init__(self, cn, ca_name: str, cert_data, revoked=False):
self._cn = cn self._cn = cn
self._ca_name = ca_name self._ca_name = ca_name
self._cert_data = cert_data self._cert_data = cert_data
self._revoked = revoked
self._cert_data.not_valid_after.replace(tzinfo=tz.tzutc())
self._cert_data.not_valid_before.replace(tzinfo=tz.tzutc())
@property @property
def cn(self): def cn(self):
@ -151,19 +155,35 @@ class Certificate(object):
return self._ca_name return self._ca_name
@property @property
def not_valid_before(self): def not_valid_before(self) -> datetime:
return self._cert_data.not_valid_before return self._cert_data.not_valid_before.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None)
@property @property
def not_valid_after(self): def not_valid_after(self) -> datetime:
return self._cert_data.not_valid_after return self._cert_data.not_valid_after.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None)
def fingerprint(self, algorithm=hashes.SHA256()): @property
def serial_number(self) -> int:
return self._cert_data.serial_number
@property
def serial_number_hex(self) -> str:
return f'{self._cert_data.serial_number:X}'
def fingerprint(self, algorithm=hashes.SHA256()) -> bytes:
return self._cert_data.fingerprint(algorithm) return self._cert_data.fingerprint(algorithm)
def pem(self): @property
def is_valid(self) -> bool:
return self.not_valid_after > datetime.now() and not self._revoked
def pem(self) -> str:
return self._cert_data.public_bytes(encoding=serialization.Encoding.PEM).decode() return self._cert_data.public_bytes(encoding=serialization.Encoding.PEM).decode()
@property
def raw(self):
return self._cert_data
def __str__(self): def __str__(self):
return f'Certificate(cn={self._cn}, ca_name={self._ca_name}, not_valid_before={self.not_valid_before}, not_valid_after={self.not_valid_after})' return f'Certificate(cn={self._cn}, ca_name={self._ca_name}, not_valid_before={self.not_valid_before}, not_valid_after={self.not_valid_after})'

View file

@ -11,6 +11,8 @@ import os
import string import string
import re import re
import datetime import datetime
from dateutil import tz
from operator import attrgetter
import logging import logging
from .model import Service, User, Certificate from .model import Service, User, Certificate
@ -52,6 +54,7 @@ class Pki(object):
ca_private_key = self._ensure_private_key(ca_name) ca_private_key = self._ensure_private_key(ca_name)
ca_cert = self._ensure_ca_cert(ca_name, ca_private_key) ca_cert = self._ensure_ca_cert(ca_name, ca_private_key)
self.update_crl(ca_name)
pki_path = self._pki_path / ca_name pki_path = self._pki_path / ca_name
if not pki_path.exists(): if not pki_path.exists():
@ -62,15 +65,39 @@ class Pki(object):
def get_client_certs(self, user: User, service: Service): def get_client_certs(self, user: User, service: Service):
pki_path = self._pki_path / service.name pki_path = self._pki_path / service.name
certs = [] certs = []
crl = self._load_ca_crl(service.name)
for cert_path in pki_path.glob(f'{user.username}*.crt.pem'): for cert_path in pki_path.glob(f'{user.username}*.crt.pem'):
print(cert_path)
with cert_path.open('rb') as cert_fd: with cert_path.open('rb') as cert_fd:
cert_data = x509.load_pem_x509_certificate( cert_data = x509.load_pem_x509_certificate(
cert_fd.read(), cert_fd.read(),
backend=default_backend()) backend=default_backend())
cert = Certificate(user.username, service.name, cert_data) revoked = crl.get_revoked_certificate_by_serial_number(
cert_data.serial_number) is not None
cert = Certificate(
user.username, service.name, cert_data, revoked)
certs.append(cert) certs.append(cert)
return certs
return sorted(certs, key=attrgetter('not_valid_before'), reverse=True)
def get_crl(self, service: Service):
return self._load_ca_crl(service.name)
def get_client_cert(self, user: User, service: Service, serial_number: str) -> Certificate:
pki_path = self._pki_path / service.name
cert_path = pki_path / f'{user.username}-{serial_number}.crt.pem'
crl = self._load_ca_crl(service.name)
with cert_path.open('rb') as cert_fd:
cert_data = x509.load_pem_x509_certificate(
cert_fd.read(),
backend=default_backend())
revoked = crl.get_revoked_certificate_by_serial_number(
cert_data.serial_number) is None
cert = Certificate(user.username, service.name, cert_data, revoked)
return cert
def revoke_certificate(self, cert: Certificate):
ca_private_key = self._ensure_private_key(cert.ca_name)
self._revoke_cert(ca_private_key, cert)
def signing_publickey(self, user: User, service: Service, publickey: str, valid_time=DAY*365): def signing_publickey(self, user: User, service: Service, publickey: str, valid_time=DAY*365):
_public_key = serialization.load_pem_public_key( _public_key = serialization.load_pem_public_key(
@ -81,7 +108,7 @@ class Pki(object):
username = str(user.username) username = str(user.username)
config = service.pki_config #TODO use this config config = service.pki_config #TODO use this config
domain = self._domain domain = self._domain
not_valid_before = datetime.datetime.now() not_valid_before = datetime.datetime.utcnow()
ca_public_key = ca_private_key.public_key() ca_public_key = ca_private_key.public_key()
end_entity_cert_builder = x509.CertificateBuilder().\ end_entity_cert_builder = x509.CertificateBuilder().\
@ -124,6 +151,15 @@ class Pki(object):
add_extension( add_extension(
x509.SubjectKeyIdentifier.from_public_key(_public_key), x509.SubjectKeyIdentifier.from_public_key(_public_key),
critical=False).\ critical=False).\
add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[x509.UniformResourceIdentifier(f'http://crl.{self._domain}/{ca_name}.crl')],
relative_name=None,
crl_issuer=None,
reasons=None)
]),
critical=False).\
add_extension( add_extension(
x509.AuthorityInformationAccess([ x509.AuthorityInformationAccess([
x509.AccessDescription( x509.AccessDescription(
@ -142,9 +178,9 @@ class Pki(object):
backend=default_backend() backend=default_backend()
) )
fingerprint =end_entity_cert.fingerprint(hashes.SHA256()).hex() serial_number = f'{end_entity_cert.serial_number:X}'
end_entity_cert_filename = self._pki_path / ca_name / \ end_entity_cert_filename = self._pki_path / ca_name / \
f'{safe_filename(username)}-{fingerprint}.crt.pem' f'{safe_filename(username)}-{serial_number}.crt.pem'
# save cert # save cert
with end_entity_cert_filename.open("wb") as end_entity_cert_file: with end_entity_cert_filename.open("wb") as end_entity_cert_file:
end_entity_cert_file.write( end_entity_cert_file.write(
@ -224,3 +260,65 @@ class Pki(object):
ca_cert.public_bytes(encoding=serialization.Encoding.PEM)) ca_cert.public_bytes(encoding=serialization.Encoding.PEM))
assert isinstance(ca_cert, x509.Certificate) assert isinstance(ca_cert, x509.Certificate)
return ca_cert return ca_cert
def _revoke_cert(self, ca_private_key, cert):
crl = self._load_ca_crl(cert.ca_name)
builder = self._builder_crl(cert.ca_name, crl)
revoked_certificate = x509.RevokedCertificateBuilder().serial_number(
cert.raw.serial_number
).revocation_date(
datetime.datetime.now(tz.tzlocal())
).build(default_backend())
builder = builder.add_revoked_certificate(revoked_certificate)
crl = self._crl_save(cert.ca_name, ca_private_key, builder)
foobar = crl.get_revoked_certificate_by_serial_number(cert.raw.serial_number)
def _get_path_crl(self, ca_name):
return self._pki_path / f'{ca_name}.crl.pem'
def _crl_save(self, ca_name, private_key, builder):
crl = builder.sign(
private_key=private_key,
algorithm=hashes.SHA256(),
backend=default_backend()
)
ca_crl_filename = self._get_path_crl(ca_name)
with open(ca_crl_filename, "wb") as ca_crl_file:
ca_crl_file.write(
crl.public_bytes(encoding=serialization.Encoding.PEM))
return crl
def _builder_crl(self, ca_name, old_crl=None):
builder = x509.CertificateRevocationListBuilder()
iname = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, f'Lenticular Cloud CA - {ca_name}'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME,
'Lenticluar Cloud'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME,
ca_name),
])
builder = builder.issuer_name(iname)
builder = builder.last_update(datetime.datetime.now(tz.tzlocal()))
builder = builder.next_update(datetime.datetime.now(tz.tzlocal()) + DAY)
if old_crl is not None:
for revoked_certificate in old_crl:
builder = builder.add_revoked_certificate(revoked_certificate)
return builder
def update_crl(self, ca_name):
ca_private_key = self._ensure_private_key(ca_name)
crl = self._load_ca_crl(ca_name)
builder = self._builder_crl(ca_name, crl)
return self._crl_save(ca_name, ca_private_key, builder)
def _load_ca_crl(self, ca_name):
ca_crl_filename = self._get_path_crl(ca_name)
if ca_crl_filename.exists():
with ca_crl_filename.open("rb") as ca_crl_file:
crl = x509.load_pem_x509_crl(
ca_crl_file.read(),
backend=default_backend())
return crl

View file

@ -3,3 +3,4 @@
from .auth import auth_views from .auth import auth_views
from .frontend import frontend_views, init_login_manager from .frontend import frontend_views, init_login_manager
from .api import api_views from .api import api_views
from .pki import pki_views

View file

@ -126,12 +126,26 @@ def client_cert():
return render_template('frontend/client_cert.html.j2', services=current_app.lenticular_services, client_certs=client_certs) return render_template('frontend/client_cert.html.j2', services=current_app.lenticular_services, client_certs=client_certs)
@frontend_views.route('/client_cert/<service_name>/<fingerprint>') @frontend_views.route('/client_cert/<service_name>/<serial_number>')
@login_required @login_required
def get_client_cert(service_name, fingerprint): def get_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name] service = current_app.lenticular_services[service_name]
current_app.pki.get_client_cert(current_user, service, fingerprint) cert = current_app.pki.get_client_cert(
pass current_user, service, serial_number)
return jsonify({
'data': {
'pem': cert.pem()}
})
@frontend_views.route('/client_cert/<service_name>/<serial_number>', methods=['DELETE'])
@login_required
def revoke_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name]
cert = current_app.pki.get_client_cert(
current_user, service, serial_number)
current_app.pki.revoke_certificate(cert)
return jsonify({})
@frontend_views.route( @frontend_views.route(
@ -160,7 +174,8 @@ def client_cert_new(service_name):
'errors': form.errors 'errors': form.errors
}) })
return render_template('frontend/client_cert_new.html.j2', return render_template(
'frontend/client_cert_new.html.j2',
service=service, service=service,
form=form) form=form)
@ -172,7 +187,7 @@ def totp():
return render_template('frontend/totp.html.j2', delete_form=delete_form) return render_template('frontend/totp.html.j2', delete_form=delete_form)
@frontend_views.route('/totp/new', methods=['GET','POST']) @frontend_views.route('/totp/new', methods=['GET', 'POST'])
@login_required @login_required
def totp_new(): def totp_new():
form = TOTPForm() form = TOTPForm()

View file

@ -0,0 +1,38 @@
import flask
from flask import Blueprint, redirect, request
from flask import current_app, session
from flask import jsonify
from flask.helpers import make_response
from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for
from flask_login import login_required, login_user, logout_user
from werkzeug.utils import redirect
import logging
from urllib.parse import urlparse
from base64 import b64decode, b64encode
import ory_hydra_client as hydra
from requests_oauthlib.oauth2_session import OAuth2Session
import requests
from cryptography.hazmat.primitives import serialization
from ..model import User, SecurityUser
from ..model_db import User as DbUser
from ..form.login import LoginForm
from ..auth_providers import LdapAuthProvider
pki_views = Blueprint('pki', __name__, url_prefix='/')
@pki_views.route('/<service_name>.crl')
def crl(service_name: str):
service = current_app.lenticular_services[service_name]
crl = current_app.pki.get_crl(service)
return crl.public_bytes(encoding=serialization.Encoding.DER)

1304
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,8 +7,9 @@
}, },
"author": "TuxCoder", "author": "TuxCoder",
"license": "GPLv3", "license": "GPLv3",
"devDependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"bootstrap": "^4.4.1",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"jquery": "^3.5.0", "jquery": "^3.5.0",
@ -28,7 +29,7 @@
"webpack": "^4.43.0", "webpack": "^4.43.0",
"webpack-cli": "^3.3.11" "webpack-cli": "^3.3.11"
}, },
"dependencies": { "devDependencies": {
"bootstrap": "^4.4.1" "simple-form-submit": "^1.0.22"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -15,6 +15,28 @@
{% block body %} {% block body %}
<div class='messages-box'>
</div>
<template id='confirm-dialog-template'>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger close" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-ok process">Process</button>
</div>
</div>
</div>
</template>
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -57,9 +79,9 @@
<div class="sidebar-sticky active"> <div class="sidebar-sticky active">
{#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#} {#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#}
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
</div> </div>
</nav> </nav>
{% endif %} {% endif %}
@ -76,10 +98,12 @@
</main> </main>
</div> </div>
</div> </div>
<div class="mt-5 row justify-content-center"> <div class='container'>
<footer> <div class="mt-5 row justify-content-center">
<span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. &copy;2019') }}</span> <footer>
</footer> <span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. &copy;') + '2020' }}</span>
</footer>
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -21,18 +21,27 @@
<tr> <tr>
<th>not valid before</th> <th>not valid before</th>
<th>not valid after</th> <th>not valid after</th>
<th>fingerprint<th> <th>serial_number<th>
<th> <th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for cert in client_certs[service.name] %} {% for cert in client_certs[service.name] %}
<tr> <tr {{ 'class="table-warning"' if not cert.is_valid else ''}}>
<td>{{ cert.not_valid_before }}</td> <td>{{ cert.not_valid_before }}</td>
<td>{{ cert.not_valid_after }}</td> <td>{{ cert.not_valid_after }}</td>
<td>{{ cert.fingerprint().hex() }}</td> <td>{{ cert.serial_number_hex }}</td>
<td>
<a title="{{ gettext('Download') }}" href="{{ url_for('.get_client_cert', service_name=service.name, serial_number=cert.serial_number_hex) }}"><i class="fas fa-file-download"></i></a>
&nbsp;
{% if cert.is_valid %}
<a title="{{ gettext('Revoke')}}" href="{{ url_for('.revoke_client_cert', service_name=service.name, serial_number=cert.serial_number_hex) }}" onclick="client_cert.revoke_certificate(this.href, '{{ cert.serial_number_hex }}'); return false;"><i class="fas fa-ban"></i></a>
{% endif %}
</td>
</tr>
{% endfor %} {% endfor %}
</table> </table>
<a class="btn btn-default" href="{{ url_for('frontend.client_cert_new', service_name=service.name) }}"> <a class="btn btn-primary" href="{{ url_for('frontend.client_cert_new', service_name=service.name) }}">
New Certificate New Certificate
</a> </a>
</div> </div>
@ -45,7 +54,7 @@
{% block script_js %} {% block script_js %}
client_certs.init_list(); client_cert.init_list();
{% endblock %} {% endblock %}

View file

@ -9,4 +9,4 @@ logging.basicConfig(level=logging.DEBUG)
if __name__ == "__main__": if __name__ == "__main__":
#app.run(ssl_context=('https.crt', 'https.key'), debug=True, host='127.0.0.1') #app.run(ssl_context=('https.crt', 'https.key'), debug=True, host='127.0.0.1')
app.run(debug=True, host='127.0.0.1') app.run(debug=True, host='127.0.0.1', port=9090)