add more pki features, bug fixes, try not to use jquery
This commit is contained in:
parent
38932aef44
commit
6c388c8129
18 changed files with 675 additions and 1068 deletions
|
@ -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():
|
||||
|
|
|
@ -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})'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
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)
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue