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

@ -54,11 +54,12 @@ def init_app(name=None):
hydra_client = hydra.ApiClient(hydra_config)
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)
app.register_blueprint(auth_views)
app.register_blueprint(frontend_views)
app.register_blueprint(api_views)
app.register_blueprint(pki_views)
@app.before_request
def befor_request():

View file

@ -9,6 +9,7 @@ from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from collections.abc import MutableSequence
from datetime import datetime
from dateutil import tz
import pyotp
import json
@ -137,10 +138,13 @@ class Service(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._ca_name = ca_name
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
def cn(self):
@ -151,19 +155,35 @@ class Certificate(object):
return self._ca_name
@property
def not_valid_before(self):
return self._cert_data.not_valid_before
def not_valid_before(self) -> datetime:
return self._cert_data.not_valid_before.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None)
@property
def not_valid_after(self):
return self._cert_data.not_valid_after
def not_valid_after(self) -> datetime:
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)
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()
@property
def raw(self):
return self._cert_data
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})'

View file

@ -11,6 +11,8 @@ import os
import string
import re
import datetime
from dateutil import tz
from operator import attrgetter
import logging
from .model import Service, User, Certificate
@ -52,6 +54,7 @@ class Pki(object):
ca_private_key = self._ensure_private_key(ca_name)
ca_cert = self._ensure_ca_cert(ca_name, ca_private_key)
self.update_crl(ca_name)
pki_path = self._pki_path / ca_name
if not pki_path.exists():
@ -62,15 +65,39 @@ class Pki(object):
def get_client_certs(self, user: User, service: Service):
pki_path = self._pki_path / service.name
certs = []
crl = self._load_ca_crl(service.name)
for cert_path in pki_path.glob(f'{user.username}*.crt.pem'):
print(cert_path)
with cert_path.open('rb') as cert_fd:
cert_data = x509.load_pem_x509_certificate(
cert_fd.read(),
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)
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):
_public_key = serialization.load_pem_public_key(
@ -81,7 +108,7 @@ class Pki(object):
username = str(user.username)
config = service.pki_config #TODO use this config
domain = self._domain
not_valid_before = datetime.datetime.now()
not_valid_before = datetime.datetime.utcnow()
ca_public_key = ca_private_key.public_key()
end_entity_cert_builder = x509.CertificateBuilder().\
@ -124,6 +151,15 @@ class Pki(object):
add_extension(
x509.SubjectKeyIdentifier.from_public_key(_public_key),
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(
x509.AuthorityInformationAccess([
x509.AccessDescription(
@ -142,9 +178,9 @@ class Pki(object):
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 / \
f'{safe_filename(username)}-{fingerprint}.crt.pem'
f'{safe_filename(username)}-{serial_number}.crt.pem'
# save cert
with end_entity_cert_filename.open("wb") as end_entity_cert_file:
end_entity_cert_file.write(
@ -224,3 +260,65 @@ class Pki(object):
ca_cert.public_bytes(encoding=serialization.Encoding.PEM))
assert isinstance(ca_cert, x509.Certificate)
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 .frontend import frontend_views, init_login_manager
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)
@frontend_views.route('/client_cert/<service_name>/<fingerprint>')
@frontend_views.route('/client_cert/<service_name>/<serial_number>')
@login_required
def get_client_cert(service_name, fingerprint):
def get_client_cert(service_name, serial_number):
service = current_app.lenticular_services[service_name]
current_app.pki.get_client_cert(current_user, service, fingerprint)
pass
cert = current_app.pki.get_client_cert(
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(
@ -160,7 +174,8 @@ def client_cert_new(service_name):
'errors': form.errors
})
return render_template('frontend/client_cert_new.html.j2',
return render_template(
'frontend/client_cert_new.html.j2',
service=service,
form=form)
@ -172,7 +187,7 @@ def totp():
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
def totp_new():
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)