add more pki features, bug fixes, try not to use jquery
This commit is contained in:
parent
38932aef44
commit
6c388c8129
|
@ -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'
|
||||||
|
|
45
browser_app/confirm-modal.js
Normal file
45
browser_app/confirm-modal.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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})'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
38
lenticular_cloud/views/pki.py
Normal file
38
lenticular_cloud/views/pki.py
Normal 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
1304
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
@ -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">×</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. ©2019') }}</span>
|
<footer>
|
||||||
</footer>
|
<span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. ©') + '2020' }}</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
2
wsgi.py
2
wsgi.py
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue