Commit ecd07d22 authored by Vincent Pelletier's avatar Vincent Pelletier

all: Major rework.

- Re-evaluate feature set and REST API.
- switch duration units to days, which are more meaningful than sticking to
  ISO units in this context.
- Implement the "cau" half of "caucase".
  As a consequence flask password authentication mechanism is not needed
  anymore. As HTML UI is not required internally to caucase, and as
  sqlalchemy is not used to its full extend, get rid of these
  dependencies altogether.
- Implement REST HTTP/HTTPS stand-alone server as a layer above WSGI
  application, and integrate HTTPS certificate issuance and renewal
  mechanism to simplify deployment: no middleware needed, so from
  gunicorn dependency.
- Use standard python modules for http client needs.
- Re-evaluate data retention options:
  - unsigned CSRs are kept forever
  - CRTs are stored in CSR table, and a 24 hour expiration is set
  - CA CRTs: (unchanged, expire when past validity period)
  - CRLs: (unchanged, expire when past validity period)
- Redispatch housekeeping tasks:
  - CA renewal happens when caucase is used and renewal is needed
  - CRL is flushed when re-generated
  - CSR table (containing CRTs) is cleaned when a new CSR is received
  removing completely the need for these special periodic tasks.
- Storage parameters are not stored persistently anymore, instead their
  effect (time offsets) is applied before storing (to protect against
  transient retention period reconfiguration from wiping data).
- Rework storage schema.
- Implement certificate extension propagation & filtering.
- Implement "Certificate was auto-signed" extension.
- More docstrings.
- Use a CSR as a subject & extensions template instead of only allowing
  to override the subject. Useful when renewing a certificate and when
  authenticated client wants to force (ex) a CommonName in the subject.
- Reorganise cli executable arguments to have more possible actions.
  Especially, make CA renewal systematic on command start (helps
  validating caucase URL).
- Increase the amount of sanity checks against user-provided data (ex:
  do not upload a private key which would be in the same file as the CRT
  to renew).
- Extend package classifiers.
- Get rid of revocation reason, as it seems unlikely to be filled, and
  even less likely to be read later.
- (almost) stop using pyOpenSSL. Use cryptography module instead.
  cryptography has many more features than pyOpenSSL (except for certificate
  validation, sadly), so use it. It completely removes the need to poke
  at ASN.1 ourselves, which significantly simplifies utils module, and
  certificate signature. Code is a bit more verbose when signing, but much
  simpler than before.
- add the possibility to revoke by certificate serial
- update gitignore
- include coverage configuration
- include pylint configuration
- integrate several secondary command:
  - caucase-probe to quickly check server presence and basic
    functionality, so automated deployments can easily auto-check
  - caucase-monitor to automate key initial request and renewal
  - caucase-rerequest to allow full flexibility over certificate request
    content without ever transfering private keys
- add a secure backup generation mechanism
- add a README describing the design
parent 0da27fdc
[run]
branch = true
concurrency =
thread
multiprocessing
parallel = true
/htmlcov/
/cover/
/.eggs/
/.coverage
.*.swp
*.pyc
[MESSAGES CONTROL]
disable=C0103,C0330
# C0103 Disable "Invalid name "%s" (should match %s)"
# C0330 Disable "bad-continuation"
[FORMAT]
indent-string=" "
0.2.0 (2017-08-XX)
==================
* implement the "cau" half of "caucase"
* massive rework: removal of flask dependency, removal of HTML UI, rework of
the REST API, rework of the CLI tools, rework of the WGSI application,
incomatible redesign of the database.
0.1.4 (2017-07-21)
==================
* caucase web parameter 'auto-sign-csr-amount' can be used to set how many csr must be signed automatically.
......
include CHANGES.txt
recursive-include caucase/templates *.html
recursive-include caucase/static *.css *.png *.js *.gif
include COPYING
=======
caucase
=======
Certificate Authority for Users, Certificate Authority for SErvices
Overview
========
The goal of caucase is to automate certificate issuance and renewal without
constraining how the certificate will be used.
For example, there is no assumption that the certificate will be used to
secure HTTP, nor to serve anything at all: you may need certificates to
authenticate users, or sign your mails, or secure an SQL server socket.
As an unfortunate consequence, it is not possible for caucase to automatically
validate a signing request content against a service (ex: as one could check
the certificate for an HTTPS service was requested by someone with the ability
to make it serve a special file).
This also means that, while caucase imposes RFC-recommended constraints on many
certificate fields and extensions to be worthy of trust, it imposes no
constraint at all on subject and alternate subject certificate fields.
To still allow certificates to be used, caucase uses itself to authenticate
users (humans or otherwise) who implement the validation procedure: they tell
caucase what certificates to emit. Once done, any certificate can be
prolungated at a simple request of the key holder while the to-renew
certificate is still valid (not expired, not revoked).
Bootstrapping the system (creating the first service certificate for
_`caucased` to operate on HTTPS, and creating the first user certificate to
control further certificate issuance) works by caucase automatically signing a
set number of certificates upon submission.
Vocabulary
==========
Caucase manipulates the following asymetric cryptography concepts.
- Key pair: A private key and corresponding public key. The public key can be
derived from the private key, but not the other way around. As a consequence,
the private key is itself considered to be a key pair.
- Certificate: A certificate is the assurante, by a certificate authority,
that a given public key and set of attributes belong to an authorised entity.
Abbreviated cert or crt. A certificate is by definition signed by a CA.
- Certificate Authority: An entry, arbitrarily trusted (but worthy of trust by
its actions and decision) which can issue certificates. Abbreviated CA.
- Certificate signing request: A document produced by an entity desiring to get
certified, which they send to a certificate authority. The certificate signing
request contains the public key and desired set of attributes that the CA
should pronounce itself on. The CA has all liberty to issue a different set
of attiributes, or to not issue a certificate.
- Certificate revocation list: Lists the certificates which were issued by a CA
but which should not be trusted annymore. This can happen for a variety of
reasons: the private key was compromised, or its owneing entity should not be
trusted anymore (ex: entity's permission to access to protected service was
revoked).
- PEM: A serialisation mechanism commonly used for various cryptographic data
pieces. It relies on base64 so it is 7-bits-safe (unlike DER), and is very
commonly supported. Caucase exclusively uses PEM format.
Validity period
===============
Cryptographic keys wear out as are used and and as they age.
Of course, they do not bit-rot nor become thinner with use. But each time one
uses a key and each minute an attacker had access to a public key, fractions
of the private key bits are inevitably leaked, weakening it overall.
So keys must be renewed periodically to preserve intended security level. So
there is a limited life span to each certificate, including the ones emitted by
caucase.
The unit duration for caucase-emitted certificates is the "normal" certificate
life span. It default to 93 days from the moment the certificate was signed,
or about 3 months.
Then the CA certificate has a default life span of 4 "normal" certificate
validity periods. As CA renewal happens in caucase without x509-level cross
signing (by decision, to avoid relying on intermediate CA support on
certificate presenter side and instead rely on more widespread
multi-CA-certificate support on virifier side), there is a hard lower bound of
3 validity periods, under which the CA certificate cannot be reliably renewed
without risking certificate validation issues for emitted "normal"
certificates. CA certificate renewal is composed of 2 phases:
- Passive distribution phase: current CA certificate has a remaining life span
of less than 2 "normal" certificate life spans: a new CA certificate is
generated and distributed on-demand (on "normal" certificate renewal and
issuance, on CRL retrieval with caucase tools...), but not used to sign
anything.
- Active use phase: new CA certificate is valid for more than one "normal"
certificate life span. This means that all existing certificates which are
still in active use had to be renewed at least once since the new CA
certificate exists. This means all the certificate holders had the
opportunity to learn about the new CA certificate. So the new CA certificate
starts being used to sign new certificates, and the old CA certificate falls
out of use as its signed "normal" certificates expire.
By default, all caucase tools will generate a new private key unrelated to the
previous one on each certificat renewal.
Lastly, there is another limited validity period, although not for the same
reasons: the list of revoked certificates also has a maximum life span. In
caucase, the CRL is re-generated whenever it is requested and:
- there is no previous CRL
- previous CRL expired
- any revocation happened since previous CRL was created
Commands
========
Caucase provides several commands to work with certificates.
caucase
+++++++
Reference caucase "one-shot" client.
This command is intended to be used for isolated actions:
- listing and signing pending certificate signature requests
- revoking certificates
It is also able to submit certificate signing requests, retrieve signed
certificates, requesting certificate renewals and updating both
CA certificates and revocation lists, but you may be interested in using
_`caucase-monitor` for this instead.
caucase-monitor
+++++++++++++++
Reference caucase certificate renewal daemon.
Monitors a key pair, corresponding CA certificate and CRL, and renew them
before expiration.
When the key-pair lacks a signed certificate, issues a pre-existing CSR to
caucase server and waits for the certificate to be issued.
caucase-probe
+++++++++++++
Caucase server availability tester.
Performs minimal checks to verify a caucase server is available at given URL.
caucase-rerequest
+++++++++++++++++
Utility allowing to re-issue a CSR using a locally-generated private key.
Intended to be used in conjunction with _`caucase-monitor` when user cannot
generate the CSR on the system where the certificate is desired (ex: automated
HTTPS server deployment), where user is not the intended audience for
caucase-produced certificate:
- User generates a CSR on their own system, and signs it with any key (it will
not be needed later
- User sends the CSR to the system where the certificate is desired
- User gets caucase-rerequest to run on this CSR, producing a new private key
and a CSR similar to issued one, but signed with this new private key
- From then on, caucase-monitor can take over
This way, no private key left their original system, and user could still
freely customise certificate extensions.
caucase-key-id
++++++++++++++
Utility displaying the identifier of given key, or the identifier of keys
involved in given backup file.
Allows identifying users which hold a private key candidate for restoring a
caucased backup (see _`Restoration procedure`).
caucased
++++++++
Reference caucase server daemon.
This daemon provides access to both CAU and CAS services over both HTTP and
HTTPS.
It handles its own certificate issuance and renewal, so there is no need to use
_`caucase-monitor` for this service.
Backups
-------
Loosing the CA private key prevents issuing any new certificate trusted by
services which trusted the CA. Also, it prevents issuing any new CRL.
Recovering from such total loss requires starting a new CA and rolling it out
to all services which used the previous one. This is very time-costly.
So backups are required.
On the other hand, if someone gets their hand on the CA private key, they can
issue certificates for themselves, allowing them to authenticate with services
trusting the CA managed by caucase - including caucased itself if they issue a
user certificate: they can then revoke existing certificates and cause a lot of
damage.
So backups cannot happen in clear text, they must be encrypted.
But the danger of encrypted backups is that by definition they become worthless
if they cannot be decrypted. So as many (trusted) entities as possible should
be granted the ability to decrypt the backups.
The solution proposed by caucased is to encrypt produced backups in a way which
allows any of the caucase users to decrypt the archive.
As these users are already entrusted with issuing certificates, this puts
only a little more power in their hands than they already have. The little
extra power they get is that by having unrestricted access to the CA private
key they can issue certificates bypassing all caucase restrictions. The
proposed parade is to only make the backups available to a limited subset of
caucase users when there is an actual disaster, and otherwise keep it out of
their reach. This mechanism is not handled by caucase.
As there are few trusted users, caucase can keep their still-valid certificates
in its database for the duration of their validity with minimal size cost.
Backup procedure
----------------
Backups happen periodically as long as caucased is running. See
`--backup-period` and `--backup-directory`.
As discussed above, produced files should be kept out of reach of caucase
users until a disaster happens.
Restoration procedure
---------------------
See `--restore-backup`.
To restore, one of the trusted users must voluntarily compromise their own
private key, providing it to the administrator in charge of the restoration
procedure. Restoration procedure will hence immediately revoke their
certificate. They must also provide a CSR generated with a different private
key, so that caucase can provide them with a new certificate, so they keep
their access only via different credentials.
- admin identifies the list of keys which can decipher a backup, and broadcasts
that list to key holders
- key holders manifest themselves
- admin picks a key holder, requests them to provide their eixsting private key
and to generate a new key and accompanying csr
- key holder provide requested items
- admin initiates restoration with `--restore-backup` and provides key holder
with the csr_id so they can fetch their new certificate using caucase
protocol
Backup file format
------------------
- 64bits: 'caucase\0' magic string
- 32bits LE: header length
- header: json-encoded header (see below)
- encrypted byte stream (aka payload)
Header schema (inspired from s/mime, but s/mime tools available do not
support at least iterative production or iterative generation)::
{
"description": "Caucase backup header",
"required": ["algorithm", "key_list"],
"properties": {
"cipher": {
"description": "Symetric ciher used for payload",
"required": ["name"],
"properties": {
"name":
"enum": ["aes256_cbc_pkcs7_hmac_10M_sha256"],
"type": "string"
},
"parameter": {
"description": "Name-dependend clear cipher parameter (ex: IV)",
"type": "string"
}
}
"type": "object"
},
"key_list": {
"description": "Content key, encrypted with public keys",
"minItems": 1,
"items": {
"required": ["id", "cipher", "key"],
"properties": {
"id": {
"description": "Hex-encoded sha1 hash of the public key",
"type": "string"
},
"cipher": {
"description": "Asymetric cipher used for symetric key",
"required": ["name"],
"properties": {
"name": {
"enum": ["rsa_oaep_sha1_mgf1_sha1"],
"type": "string"
}
},
"type": "object"
}
"key": {
"description": "Hex-encoded encrypted concatenation of signing and symetric encryption keys",
"type": "string"
}
},
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
Blocker for 1.0
===============
- After pyca/cryptography 21st release: Make is_signature_valid call mandatory in caucase.utils.load_crl .
- After pyca/cryptography later release (code not fixed yet): Enable CRL distribution point extension when it tolerates literal IPv6 in the URL.
Eventually
==========
- Become an OCSP responder (requires support in other libraries - likely pyca/cryptography).
......@@ -15,23 +15,6 @@
#
# You should have received a copy of the GNU General Public License
# 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__)
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# Use default value so SQLALCHEMY will not warn because there is not db_uri
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///ca.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
from caucase import web, storage
\ No newline at end of file
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
# -*- coding: utf-8 -*-
# This file is part of caucase
# Copyright (C) 2017 Nexedi
# Alain Takoudjou <alain.takoudjou@nexedi.com>
......@@ -16,541 +15,907 @@
#
# You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
from __future__ import absolute_import
import datetime
import json
import os
import sys
import subprocess
import re
import time
import uuid
import errno
import tempfile
from OpenSSL import crypto, SSL
import traceback
from pyasn1.codec.der import encoder as der_encoder
from pyasn1.type import tag
from pyasn1_modules import rfc2459
from datetime import datetime, timedelta
from caucase.exceptions import (ExpiredCertificate, NotFound,
BadCertificateSigningRequest, CertificateVerificationError)
from caucase import utils
MIN_CA_RENEW_PERIOD = 2
DEFAULT_DIGEST_LIST = ['sha256', 'sha384', 'sha512']
SUBJECT_KEY_LIST = ['C', 'ST', 'L', 'OU', 'O', 'CN', 'emailAddress']
def getX509NameFromDict(**name_dict):
"""
Return a new X509Name with the given attributes.
"""
# XXX There's no other way to get a new X509Name.
name = crypto.X509().get_subject()
for key, value in name_dict.items():
setattr(name, key, value)
return name
import struct
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes, hmac
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from . import utils
from .exceptions import CertificateVerificationError
__all__ = ('CertificateAuthority', )
_cryptography_backend = default_backend()
_AUTO_SIGNED_NO = 0
_AUTO_SIGNED_YES = 1
_AUTO_SIGNED_PASSTHROUGH = 2
_SUBJECT_OID_DICT = {
# pylint: disable=bad-whitespace
'C' : x509.oid.NameOID.COUNTRY_NAME,
'O' : x509.oid.NameOID.ORGANIZATION_NAME,
'OU': x509.oid.NameOID.ORGANIZATIONAL_UNIT_NAME,
'ST': x509.oid.NameOID.STATE_OR_PROVINCE_NAME,
'CN': x509.oid.NameOID.COMMON_NAME,
'L' : x509.oid.NameOID.LOCALITY_NAME,
'SN': x509.oid.NameOID.SURNAME,
'GN': x509.oid.NameOID.GIVEN_NAME,
# pylint: enable=bad-whitespace
}
_BACKUP_MAGIC = 'caucase\0'
_CONFIG_NAME_AUTO_SIGN_CSR_AMOUNT = 'auto_sign_csr_amount'
def Extension(value, critical):
"""
Avoid oid redundant parameter when creating an extension.
"""
return x509.Extension(
oid=value.oid,
critical=critical,
value=value,
)
class CertificateAuthority(object):
def __init__(self, storage, ca_life_period, ca_renew_period,
crt_life_time, crl_renew_period, digest_list=None,
crl_base_url=None, ca_subject='',
max_csr_amount=50, crt_keep_time=0,
auto_sign_csr_amount=0):
self._storage = storage
self.ca_life_period = ca_life_period
self.digest_list = digest_list
self.crt_life_time = crt_life_time
self.crl_renew_period = crl_renew_period
self.ca_renew_period = ca_renew_period
self.default_digest = 'sha256'
self.crl_base_url = crl_base_url
self.auto_sign_csr_amount = auto_sign_csr_amount
self.extension_manager = utils.X509Extension()
self.mandatory_subject_key_list = ['CN']
self.ca_subject_dict = self._getCASubjectDict(ca_subject)
# XXX - ERR_SSL_SERVER_CERT_BAD_FORMAT on browser
# Because if two certificate has the same serial from a CA with the same CN
# self.ca_subject_dict['CN'] = '%s %s' % (self.ca_subject_dict['CN'], int(time.time()))
if not self.digest_list:
self.digest_list = DEFAULT_DIGEST_LIST
if self.ca_life_period < MIN_CA_RENEW_PERIOD:
raise ValueError("'ca_life_period' value should be upper than %s" % MIN_CA_RENEW_PERIOD)
if self.crl_renew_period > 1:
raise ValueError("'crl_renew_period' is too high and should be less than a certificate life time.")
self.crl_life_time = int(self.crt_life_time * self.crl_renew_period)
self.ca_life_time = int(self.crt_life_time * self.ca_life_period)
self.ca_renew_time = int(self.crt_life_time * self.ca_renew_period)
self._ca_key_pairs_list = self._storage.getCAKeyPairList()
if not self._ca_key_pairs_list:
self.createCAKeyPair()
def _getCASubjectDict(self, ca_subject):
"""
Parse CA Subject from provided sting format
Ex: /C=XX/ST=State/L=City/OU=OUnit/O=Company/CN=CA Auth/emailAddress=xx@example.com
This class implements CA policy and lifetime logic:
- how CA key pair is generated
- what x509.3 extensions and attributes are enforced on signed certificates
- CA and CRL automated renewal
"""
def __init__(
self,
storage,
ca_subject_dict=(),
ca_key_size=2048,
crt_life_time=31 * 3, # Approximately 3 months
ca_life_period=4, # Approximately a year
crl_renew_period=0.33, # Approximately a month
crl_base_url=None,
digest_list=utils.DEFAULT_DIGEST_LIST,
auto_sign_csr_amount=0,
lock_auto_sign_csr_amount=False,
):
"""
storage (caucase.storage.Storage)
Persistent storage of certificate authority data.
ca_subject_dict (dict)
Items to use as Certificate Authority certificate subject.
Supported keys are: C, O, OU, ST, CN, L, SN, GN.
ca_key_size (int)
Number of bits to use as Certificate Authority key.
crt_life_time (float)
Validity duration for every issued certificate, in days.
ca_life_period (float)
Number of crt_life_time periods for which Certificate Authority
certificate will be valid.
Must be greater than 3 to allow smooth rollout.
crl_renew_period (float)
Number of crt_life_time periods for which a revocation list is
valid for.
crl_base_url (str)
The CRL distribution URL to include in signed certificates.
None to not declare a CRL distribution point in generated certificates.
Revocations are be functional even if this is None.
digest_list (list of str)
List of digest algorithms considered acceptable for authenticating
renewal and revocation requests, and CA renewal list responses.
The first item will be the one used, others are accepted but not used.
auto_sign_csr_amount (int)
Automatically sign the first <auto_sign_csr_amount> CSRs.
As certificate gets unconditionally emitted and only vital attributes
and extensions are forced during signature, you should choose the
smallest amount possible to get a functional service.
For a typical HTTP(S) caucase service, 1 should be enough for CAS usage
(first service certificate being to serve HTTPS for caucase), and 1 for
CAU usage (first user, which can then sign more user certificate
requests).
To verify nothing accessed the service before intended automated
requests, check issued certificate has an extension with OID:
2.25.285541874270823339875695650038637483517.0
(a message is printed when retrieving the certificate)
This mark is propagated during certificate renewal.
lock_auto_sign_csr_amount (bool)
When given with a true value, auto_sign_csr_amount is stored and the
value given on later instanciation will be ignored.
"""
ca_subject_dict = {}
regex = r"\/([C|ST|L|O|OU|CN|emailAddress]+)=([\w\s\@\.\d\-_\(\)\,\+:']+)"
matches = re.finditer(regex, ca_subject)
for match in matches:
key = match.group(1)
if not key in SUBJECT_KEY_LIST:
raise ValueError("Item %r is not a valid CA Subject key, please" \
"Check that the provided key is in %s" % (key,
SUBJECT_KEY_LIST))
ca_subject_dict[key] = match.group(2)
for key in self.mandatory_subject_key_list:
if key not in ca_subject_dict:
raise ValueError("The subject key '%r' is mandatory." % key)
self._storage = storage
if lock_auto_sign_csr_amount:
storage.setConfigOnce(
_CONFIG_NAME_AUTO_SIGN_CSR_AMOUNT,
auto_sign_csr_amount,
)
self._auto_sign_csr_amount = int(storage.getConfigOnce(
_CONFIG_NAME_AUTO_SIGN_CSR_AMOUNT,
auto_sign_csr_amount,
))
return ca_subject_dict
self._ca_key_size = ca_key_size
self._digest_list = digest_list
self._default_digest_class = getattr(hashes, self.digest_list[0].upper())
self._crt_life_time = datetime.timedelta(crt_life_time, 0)
self._crl_base_url = crl_base_url
self._ca_subject = x509.Name([
x509.NameAttribute(
oid=_SUBJECT_OID_DICT[key],
value=value,
)
for key, value in dict(ca_subject_dict).iteritems()
])
if ca_life_period < 3:
raise ValueError("ca_life_period must be >= 3 to allow CA rollout")
self._crl_life_time = datetime.timedelta(crt_life_time * crl_renew_period, 0)
self._ca_life_time = datetime.timedelta(crt_life_time * ca_life_period, 0)
self._loadCAKeyPairList()
self._renewCAIfNeeded()
@property
def digest_list(self):
"""
Read-only access to digest_list ctor parameter.
"""
return list(self._digest_list)
def _loadCAKeyPairList(self):
ca_key_pair_list = []
for pem_key_pair in self._storage.getCAKeyPairList():
utils.validateCertAndKey(pem_key_pair['crt_pem'], pem_key_pair['key_pem'])
ca_key_pair_list.append({
'crt': utils.load_ca_certificate(pem_key_pair['crt_pem']),
'key': utils.load_privatekey(pem_key_pair['key_pem']),
})
self._ca_key_pairs_list = ca_key_pair_list
def renewCAKeyPair(self):
"""
Refresh instance's knowledge of database content
(as storage house-keeping may/will happen outside our control)
def createCAKeyPair(self):
"""
Create a new CA key pair.
cert = self._ca_key_pairs_list[-1]['crt']
expire_date = datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ')
renew_date = expire_date - timedelta(0, self.ca_renew_time)
if renew_date > datetime.now():
# The ca certificat should not be renewed now
return False
self.createCAKeyPair()
return True
def createCAKeyPair(self):
CA certificate renewal normally happens automatically as long as
certificates are getting signed and revocation list downloaded.
"""
Create a new ca key + certificate pair
"""
key_pair = {}
key = crypto.PKey()
# Use 2048 bits key size
key.generate_key(crypto.TYPE_RSA, 2048)
key_pair['key'] = key
ca = crypto.X509()
# 3 = v3
ca.set_version(3)
ca.set_serial_number(int(time.time()))
subject = ca.get_subject()
for name, value in self.ca_subject_dict.items():
setattr(subject, name, value)
ca.gmtime_adj_notBefore(0)
ca.gmtime_adj_notAfter(self.ca_life_time)
ca.set_issuer(ca.get_subject())
ca.set_pubkey(key)
self.extension_manager.setCaExtensions(ca)
ca.sign(key, self.default_digest)
key_pair['crt'] = ca
self._storage.storeCAKeyPair(key_pair)
self._ca_key_pairs_list = self._storage.getCAKeyPairList()
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=self._ca_key_size,
backend=_cryptography_backend,
)
public_key = private_key.public_key()
subject_key_identifier = x509.SubjectKeyIdentifier.from_public_key(
public_key,
)
now = datetime.datetime.utcnow()
certificate = x509.CertificateBuilder(
subject_name=self._ca_subject,
issuer_name=self._ca_subject,
not_valid_before=now,
not_valid_after=now + self._ca_life_time,
serial_number=x509.random_serial_number(),
public_key=public_key,
extensions=[
Extension(
x509.BasicConstraints(
ca=True,
path_length=0,
),
critical=True, # "MUST mark the extension as critical"
),
Extension(
x509.KeyUsage(
# pylint: disable=bad-whitespace
digital_signature =False,
content_commitment=False,
key_encipherment =False,
data_encipherment =False,
key_agreement =False,
key_cert_sign =True,
crl_sign =True,
encipher_only =False,
decipher_only =False,
# pylint: enable=bad-whitespace
),
critical=True, # "SHOULD mark this extension critical"
),
# Should we make use of certificate policies ? If we do, we need to enable
# this extension and fill the values.
# Extension(
# x509.PolicyConstraints(
# require_explicit_policy=,
# inhibit_policy_mapping=,
# ),
# critical=True, # MUST mark this extension as critical
# ),
Extension(
subject_key_identifier,
critical=False, # "MUST mark this extension as non-critical"
),
Extension(
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
# Dummy extension, from_issuer_subject_key_identifier accesses .data directly
Extension(
subject_key_identifier,
critical=False,
),
),
critical=False, # "MUST mark this extension as non-critical"
),
],
).sign(
private_key=private_key,
algorithm=self._default_digest_class(),
backend=_cryptography_backend,
)
self._storage.appendCAKeyPair(
utils.datetime2timestamp(certificate.not_valid_after),
{
'key_pem': utils.dump_privatekey(private_key),
'crt_pem': utils.dump_certificate(certificate),
},
)
self._loadCAKeyPairList()
assert self._ca_key_pairs_list
def getPendingCertificateRequest(self, csr_id):
def getCertificateSigningRequest(self, csr_id):
"""
Retrieve the content of a pending signing request.
Retrieve a PEM-encoded certificate signing request.
@param csr_id: The id of CSR returned by the storage
csr_id (int)
As returned when the CSR was stored.
"""
return self._storage.getPendingCertificateRequest(csr_id)
return self._storage.getCertificateSigningRequest(csr_id)
def createCertificateSigningRequest(self, csr):
def appendCertificateSigningRequest(self, csr_pem, override_limits=False):
"""
Sanity-check CSR, stores it and generates a unique signing request
identifier (crt_id).
Store certificate signing request and return its identifier.
May trigger its signature if the quantity of submitted CSRs is less than
auto_sign_csr_amount (see __init__).
@param csr: CSR string in PEM format
csr_pem (str)
PEM-encoded certificate signing request.
"""
# Check number of already-pending signing requests
# Check if csr is self-signed
# Check it has a CN (?)
# Check its extensions
# more ?
try:
csr_pem = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr)
except crypto.Error, e:
raise BadCertificateSigningRequest(str(e))
if not hasattr(csr_pem.get_subject(), 'CN') or not csr_pem.get_subject().CN:
raise BadCertificateSigningRequest("CSR has no common name set")
# XXX check extensions
csr_id = self._storage.storeCertificateSigningRequest(csr_pem)
if self._storage.getCertificateSigningRequestAmount() <= \
self.auto_sign_csr_amount:
csr = utils.load_certificate_request(csr_pem)
# Note: requested_amount is None when a known CSR is re-submitted
csr_id, requested_amount = self._storage.appendCertificateSigningRequest(
csr_pem=csr_pem,
key_id=x509.SubjectKeyIdentifier.from_public_key(
csr.public_key(),
).digest.encode('hex'),
override_limits=override_limits,
)
if requested_amount is not None and \
requested_amount <= self._auto_sign_csr_amount:
# if allowed to sign this certificate automaticaly
self.createCertificate(csr_id)
self._createCertificate(csr_id, auto_signed=_AUTO_SIGNED_YES)
return csr_id
def deletePendingCertificateRequest(self, csr_id):
def deletePendingCertificateSigningRequest(self, csr_id):
"""
Reject a pending certificate signing request.
@param csr_id: The id of CSR returned by the storage
csr_id (int)
CSR id, as returned when the CSR was stored.
"""
self._storage.deletePendingCertificateRequest(csr_id)
self._storage.deletePendingCertificateSigningRequest(csr_id)
def getPendingCertificateRequestList(self, limit=0, with_data=False):
def getCertificateRequestList(self):
"""
Return list of pending certificate signature request
@param limit: number of element to fetch, 0 is not limit (int)
@param with_data: True or False, say if return csr PEM string associated
to others informations (bool).
Return the list of pending certificate signature requests, individually
PEM-encoded.
"""
return self._storage.getPendingCertificateRequestList(limit, with_data)
return self._storage.getCertificateSigningRequestList()
def createCertificate(self, csr_id, ca_key_pair=None, subject_dict=None):
def createCertificate(self, csr_id, template_csr=None):
"""
Generate new signed certificate. `ca_key_pair` is the CA key_pair to use
if None, use the latest CA key_pair
Sign a pending certificate signing request, storing produced certificate.
@param csr_id: CSR ID returned by storage, csr should be linked to the
new certificate (string).
@param ca_key_pair: The CA key_pair to used for signature. If None, the
latest key_pair is used.
@param subject_dict: dict of subject attributes to use in x509 subject,
if None, csr subject is used (dict).
csr_id (int)
CSR id, as returned when the CSR was stored.
template_csr (None or X509Req)
Copy extensions and subject from this CSR instead of stored one.
Useful to renew a certificate.
Public key is always copied from stored CSR.
"""
# Apply extensions (ex: "not a certificate", ...)
# Generate a certificate from the CSR
# Sign the certificate with the current CA key
csr_pem = crypto.load_certificate_request(crypto.FILETYPE_PEM,
self._storage.getPendingCertificateRequest(csr_id))
self._createCertificate(
csr_id=csr_id,
auto_signed=_AUTO_SIGNED_NO,
template_csr=template_csr,
)
# Certificate serial is the csr_id without extension .csr.pem
serial = int(csr_id[:-8], 16)
subject = None
if ca_key_pair is None:
ca_key_pair = self._ca_key_pairs_list[-1]
if subject_dict:
if subject_dict.has_key('C') and len(subject_dict['C']) != 2:
# Country code size is 2
raise ValueError("Country Code size in subject should be equal to 2.")
if not subject_dict.has_key('CN'):
raise AttributeError("Attribute 'CN' is required in subject.")
def _createCertificate(self, csr_id, auto_signed, template_csr=None):
"""
auto_signed (bool)
When True, mark certificate as having been auto-signed.
When False, prevent such mark from being set.
When None, do not filter (useful when renewing).
tempate_csr (None or X509Req)
Copy extensions and subject from this CSR instead of stored one.
Useful to renew a certificate.
Public key is always copied from stored CSR.
"""
csr_pem = self._storage.getCertificateSigningRequest(csr_id)
csr = utils.load_certificate_request(csr_pem)
if template_csr is None:
template_csr = csr
ca_key_pair = self._getCurrentCAKeypair()
ca_crt = ca_key_pair['crt']
public_key = csr.public_key()
subject_key_identifier = x509.SubjectKeyIdentifier.from_public_key(
public_key,
)
now = datetime.datetime.utcnow()
builder = x509.CertificateBuilder(
subject_name=template_csr.subject,
issuer_name=ca_crt.subject,
not_valid_before=now,
not_valid_after=now + self._crt_life_time,
serial_number=x509.random_serial_number(),
public_key=public_key,
extensions=[
Extension(
x509.BasicConstraints(
ca=False,
path_length=None,
),
critical=True, # "MAY appear as critical or non-critical"
),
Extension(
subject_key_identifier,
critical=False, # "MUST mark this extension as non-critical"
),
Extension(
x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
ca_crt.extensions.get_extension_for_class(x509.SubjectKeyIdentifier),
),
critical=False, # "MUST mark this extension as non-critical"
),
],
)
# Note: disabled because of following IPv6 bug:
# https://github.com/pyca/cryptography/issues/3863
# if self._crl_base_url:
# builder = builder.add_extension(
# x509.CRLDistributionPoints([
# x509.DistributionPoint(
# full_name=[
# x509.UniformResourceIdentifier(self._crl_base_url),
# ],
# relative_name=None,
# crl_issuer=None,
# reasons=None,
# ),
# ]),
# critical=False, # "SHOULD be non-critical"
# )
try:
subject = getX509NameFromDict(**subject_dict)
except AttributeError:
raise AttributeError("X509Name attribute not found. Subject " \
"keys should be in %r" % SUBJECT_KEY_LIST)
cert_pem = self._generateCertificateObjects(ca_key_pair,
csr_pem,
serial,
subject=subject)
crt_id = self._storage.storeCertificate(csr_id, cert_pem)
return crt_id
key_usage_extension = template_csr.extensions.get_extension_for_class(
x509.KeyUsage,
)
except x509.ExtensionNotFound:
pass
else:
key_usage = key_usage_extension.value
if key_usage.key_agreement:
encipher_only = key_usage.encipher_only
decipher_only = key_usage.decipher_only
else:
encipher_only = decipher_only = False
builder = builder.add_extension(
x509.KeyUsage(
# pylint: disable=bad-whitespace
digital_signature =key_usage.digital_signature,
content_commitment=key_usage.content_commitment,
key_encipherment =key_usage.key_encipherment,
data_encipherment =key_usage.data_encipherment,
key_agreement =key_usage.key_agreement,
key_cert_sign =False,
crl_sign =False,
encipher_only =encipher_only,
decipher_only =decipher_only,
# pylint: enable=bad-whitespace
),
critical=key_usage_extension.critical, # "SHOULD mark this extension critical"
)
try:
extended_key_usage = template_csr.extensions.get_extension_for_class(
x509.ExtendedKeyUsage,
)
except x509.ExtensionNotFound:
pass
else:
builder = builder.add_extension(
x509.ExtendedKeyUsage(
[
x for x in extended_key_usage.value
if x != x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING
]
),
critical=extended_key_usage.critical,
)
try:
subject_alt_name = template_csr.extensions.get_extension_for_class(
x509.SubjectAlternativeName,
)
except x509.ExtensionNotFound:
pass
else:
# Note: as issued certificates may be used without any subject
# validation (ex: connecting to mariadb via a variable IP), we
# voluntarily do not enforce any constraint on subjectAltName.
builder = builder.add_extension(
subject_alt_name.value,
critical=subject_alt_name.critical,
)
# subjectDirectoryAttributes ?
try:
certificate_policies = template_csr.extensions.get_extension_for_class(
x509.CertificatePolicies,
)
except x509.ExtensionNotFound:
if auto_signed == _AUTO_SIGNED_YES:
builder = builder.add_extension(
x509.CertificatePolicies([
utils.CAUCASE_POLICY_INFORMATION_AUTO_SIGNED,
]),
critical=False, # (no recommendations)
)
else:
if auto_signed == _AUTO_SIGNED_PASSTHROUGH:
# Caller is asking us to let all through, so do this.
policy_list = certificate_policies.value
else:
# Prevent any caucase extension from being smuggled, especiall the
# "auto-signed" one...
policy_list = [
x for x in certificate_policies.value
if not x.policy_identifier.dotted_string.startswith(utils.CAUCASE_OID_TOP)
]
if auto_signed == _AUTO_SIGNED_YES:
# ...but do add auto-signed extension if we are auto-signing.
policy_list.append(utils.CAUCASE_POLICY_INFORMATION_AUTO_SIGNED)
builder = builder.add_extension(
x509.CertificatePolicies(policy_list),
critical=certificate_policies.critical, # (no recommendations)
)
def getCertificate(self, crt_id):
"""
Return a Certificate string in PEM format
cert_pem = utils.dump_certificate(builder.sign(
private_key=ca_key_pair['key'],
algorithm=self._default_digest_class(),
backend=_cryptography_backend,
))
self._storage.storeCertificate(csr_id, cert_pem)
return cert_pem
@param crt_id: Certificate ID returned by storage during certificate creation
def getCertificate(self, csr_id):
"""
return self._storage.getCertificate(crt_id)
Return PEM-encoded signed certificate.
def getCertificateFromSerial(self, serial):
csr_id (int)
As returned when the corresponding CSR was stored.
"""
Return a Certificate string in PEM format
return self._storage.getCertificate(csr_id)
@param serial: serial of the certificate (string)
def _renewCAIfNeeded(self):
"""
cert = self._storage.getCertificateFromSerial(serial)
if not cert.content:
raise NotFound('Content certificate with serial %r is not found.' % (
serial,
))
return cert.content
def getSignedCertificateList(self, limit=0, with_data=False):
Create a new CA certificate if latest one has less than two
ca_life_periods of validity left.
Updates self._ca_key_pairs_list .
"""
Return list of signed certificate
if not self._ca_key_pairs_list or (
self._ca_key_pairs_list[-1]['crt'].not_valid_after - datetime.datetime.utcnow()
).total_seconds() / self._crt_life_time.total_seconds() <= 2:
# No CA certificate at all or less than 2 certificate validity periods
# left with latest CA certificate. Prepare the next one so it starts
# getting distributed.
self.createCAKeyPair()
@param limit: number of element to fetch, 0 is not limit (int)
@param with_data: True or False, say if return cert PEM string associated
to others informations (bool).
"""
return self._storage.getSignedCertificateList(limit, with_data)
def _getCurrentCAKeypair(self):
"""
Return the currently-active CA certificate key pair.
Currently-active CA certificate is the CA to use when signing. It may not
be the latest one, as all certificate holders must know the latest one
before its use can start.
"""
self._renewCAIfNeeded()
now = datetime.datetime.utcnow()
for key_pair in reversed(self._ca_key_pairs_list):
if key_pair['crt'].not_valid_before + self._crt_life_time < now:
# This CA cert is valid for more than one certificate life time,
# we can assume clients to know it (as they had to renew their
# cert at least once since it became available) so we can start
# using it.
break
else:
# No CA cert is valid for more than one certificate life time, so just pick
# the newest one.
key_pair = self._ca_key_pairs_list[-1]
return key_pair
def getCACertificate(self):
"""
Return current CA certificate
Return current CA certificate, PEM-encoded.
"""
return self._dumpCertificate(self._ca_key_pairs_list[-1]['crt'])
return utils.dump_certificate(self._getCurrentCAKeypair()['crt'])
def getValidCACertificateChain(self):
def getCACertificateList(self):
"""
Return the ca certificate chain for all valid certificates with key
Return the current list of CA certificates as X509 obbjects.
"""
result = []
iter_key_pair = iter(self._ca_key_pairs_list)
previous_key_pair = iter_key_pair.next()
for key_pair in iter_key_pair:
result.append(utils.wrap({
'old': self._dumpCertificate(previous_key_pair['crt']),
'new': self._dumpCertificate(key_pair.crt),
}, self._dumpPrivatekey(previous_key_pair['key']), self.digest_list))
return result
self._renewCAIfNeeded()
return [x['crt'] for x in self._ca_key_pairs_list]
def getCAKeypairForCertificate(self, cert):
def getValidCACertificateChain(self):
"""
Return the nearest CA key_pair to the next extension date of the cert
Return the CA certificate chain based on oldest CA certificate.
@param cert: X509 certificate
"""
cert_valid_date = datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ')
next_valid_date = datetime.utcnow() + timedelta(0, self.crt_life_time)
# check which ca certificate should be used to renew the cert
selected_keypair = None
selected_date = None
for key_pair in self._ca_key_pairs_list:
expiration_date = datetime.strptime(key_pair['crt'].get_notAfter(), '%Y%m%d%H%M%SZ')
if expiration_date < next_valid_date:
continue
if selected_date and selected_date < expiration_date:
# Only get the lowest expiration_date which cover the renew notbefore date
continue
selected_keypair = key_pair
selected_date = expiration_date
Each item in the chain is a wrapped dict with the following keys:
old (str)
N-1 certificate as PEM, used to check wrapper signature.
If item is the first in the chain, this is the oldest CA certificate
server still knows about.
new (str)
N certificate as PEM.
if selected_keypair is None:
raise ValueError("No valid CA key_pair found with validity date upper than %r certificate lifetime" %
next_valid_date)
The intent is for a client knowing one CA certificate to retrieve any newer
CA certificate and autonomously decide if it may trust them: each item is
signed with the previous certificate. The oldest CA certificate is not
returned in this list, as it cannot be signed by another one.
return selected_keypair
CA user must check that there is an uninterrupted signed path from its
already-known CA certificate to use any contained "new" certificate.
It must skip any certificate pair for which it does not already trust
an ancestor certificate.
def revokeCertificate(self, wrapped_crt):
Note: the chain may contain expired CA certificates. CA user should skip
these, and consider their signature invalid for CA chain validation
purposes.
"""
Revoke a certificate
@param wrapped_crt: The revoke request dict containing certificate to
revoke and signature algorithm used to sign the request.
self._renewCAIfNeeded()
result = []
iter_key_pair = iter(self._ca_key_pairs_list)
first_key_pair = iter_key_pair.next()
previous_crt_pem = utils.dump_certificate(first_key_pair['crt'])
previous_key = first_key_pair['key']
for key_pair in iter_key_pair:
current_crt_pem = utils.dump_certificate(key_pair['crt'])
result.append(utils.wrap(
{
"signature": "signature string for payload",
"digest": "Signature algorithm (ex: SHA256"),
"payload": dict of data: {
"revoke_crt": "Certificate to revoke",
"reason": "Revoke reason"
}
}
"""
payload = utils.unwrap(wrapped_crt, lambda x: x['revoke_crt'], self.digest_list)
try:
x509 = self._loadCertificate(payload['revoke_crt'])
except crypto.Error, e:
raise BadCertificate(str(e))
if not utils.verifyCertificateChain(x509,
[x['crt'] for x in self._ca_key_pairs_list]):
raise CertificateVerificationError("Certificate verification failed:" \
"The CA couldn't reconize the certificate to revoke.")
crt = self._loadCertificate(payload['revoke_crt'])
reason = payload['reason']
return self._storage.revokeCertificate(
utils.getSerialToInt(crt),
reason)
def revokeCertificateFromID(self, crt_id):
"""
Directly revoke a certificate from crt_id
'old_pem': previous_crt_pem,
'new_pem': current_crt_pem,
},
previous_key,
self.digest_list[0],
))
previous_key = key_pair['key']
previous_crt_pem = current_crt_pem
return result
@param crt_id: The ID of the certificate (string)
def revoke(self, crt_pem):
"""
Revoke certificate.
return self._storage.revokeCertificate(
crt_id=crt_id,
reason="")
def renew(self, wrapped_csr):
crt_pem (str)
PEM-encoded certificat to revoke.
"""
Renew a certificate
crt = utils.load_certificate(
crt_pem,
self.getCACertificateList(),
x509.load_pem_x509_crl(
self.getCertificateRevocationList(),
_cryptography_backend,
),
)
self._storage.revoke(
serial=crt.serial_number,
expiration_date=utils.datetime2timestamp(crt.not_valid_after),
)
@param wrapped_csr: The revoke request dict containing certificate to
revoke and signature algorithm used to sign the request.
{
"signature": "signature string for payload",
"digest": "Signature algorithm (ex: SHA256"),
"payload": dict of data: {
"crt": "Old certificate to renew",
"renew_csr": "New CSR to sign"
}
}
def revokeSerial(self, serial):
"""
payload = utils.unwrap(wrapped_csr, lambda x: x['crt'], self.digest_list)
csr = payload['renew_csr']
try:
x509 = self._loadCertificate(payload['crt'])
except crypto.Error, e:
raise BadCertificate(str(e))
Revoke a certificate by its serial only.
try:
csr_pem = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr)
except crypto.Error, e:
raise BadCertificateSigningRequest(str(e))
if csr_pem.get_subject().CN != x509.get_subject().CN:
raise BadCertificateSigningRequest(
"Request common name does not match replaced certificate.")
if not self._storage.getCertificateFromSerial(utils.getSerialToInt(x509)):
raise NotFound('No Certificate with serial %r and Common Name %r found.' % (
x509.get_serial_number(),
x509.get_subject().CN,
))
Revocation will expire when the latest CA certificate of this instance
expires, meaning it will stay longer in the revocation list than when
certificate expiration date can be retrieved from the certificate.
if not utils.verifyCertificateChain(x509,
[x['crt'] for x in self._ca_key_pairs_list]):
raise CertificateVerificationError("Certificate verification failed:" \
"The CA couldn't reconize signed certificate.")
csr_id = self.createCertificateSigningRequest(csr)
# sign the new certificate using a specific ca key_pair
ca_key_pair = self.getCAKeypairForCertificate(x509)
self.createCertificate(csr_id, ca_key_pair)
return csr_id
Also, there cannot be any check on the validity of the serial, typos
are accepted verbatim.
def getCertificateRevocationList(self):
"""
Generate certificate revocation list PEM
Using this method is hence not recomended.
"""
crl = self._storage.getCertificateRevocationList()
if not crl:
# Certificate revocation list needs to be regenerated
return self._createCertificateRevocationList()
return crl
def _loadCertificate(self, cert_string):
"""
Load certificate in PEM format
"""
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_string)
self._storage.revoke(
serial=serial,
expiration_date=utils.datetime2timestamp(max(
x.not_valid_after for x in self.getCACertificateList()
)),
)
def _dumpCertificate(self, cert_object):
"""
Dump certificate in PEM format
def renew(self, crt_pem, csr_pem):
"""
return crypto.dump_certificate(crypto.FILETYPE_PEM, cert_object)
Renew certificate.
def _loadPrivatekey(self, pkey):
crt_pem (str)
PEM-encoded certificate to renew.
csr_pem (str)
PEM-encoded certificate signing request.
"""
Load private key in PEM format
"""
return crypto.load_privatekey(crypto.FILETYPE_PEM, pkey)
ca_cert_list = self.getCACertificateList()
crt = utils.load_certificate(
crt_pem,
ca_cert_list,
x509.load_pem_x509_crl(
self.getCertificateRevocationList(),
_cryptography_backend,
),
)
return self._createCertificate(
csr_id=self.appendCertificateSigningRequest(csr_pem),
auto_signed=_AUTO_SIGNED_PASSTHROUGH,
# Do a dummy signature, just so we get a usable
# x509.CertificateSigningRequest instance. Use latest CA private key just
# because it is available for free (unlike generating a new one).
template_csr=x509.CertificateSigningRequestBuilder(
subject_name=crt.subject,
extensions=crt.extensions,
).sign(
private_key=self._ca_key_pairs_list[-1]['key'],
algorithm=self._default_digest_class(),
backend=_cryptography_backend,
),
)
def _dumpPrivatekey(self, pkey_object):
"""
Load private key in PEM format
def getCertificateRevocationList(self):
"""
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey_object)
Return PEM-encoded certificate revocation list.
"""
crl_pem = self._storage.getCertificateRevocationList()
if crl_pem is None:
ca_key_pair = self._getCurrentCAKeypair()
now = datetime.datetime.utcnow()
crl = x509.CertificateRevocationListBuilder(
issuer_name=ca_key_pair['crt'].issuer,
last_update=now,
next_update=now + self._crl_life_time,
extensions=[
Extension(
x509.CRLNumber(
self._storage.getNextCertificateRevocationListNumber(),
),
critical=False, # "MUST mark this extension as non-critical"
),
],
revoked_certificates=[
x509.RevokedCertificateBuilder(
serial_number=x['serial'],
revocation_date=datetime.datetime.fromtimestamp(
x['revocation_date'],
),
).build(_cryptography_backend)
for x in self._storage.getRevocationList()
],
).sign(
private_key=ca_key_pair['key'],
algorithm=self._default_digest_class(),
backend=_cryptography_backend,
)
crl_pem = crl.public_bytes(serialization.Encoding.PEM)
self._storage.storeCertificateRevocationList(
crl_pem,
expiration_date=utils.datetime2timestamp(crl.next_update),
)
return crl_pem
def _generateCertificateObjects(self, ca_key_pair, req, serial, subject=None):
class UserCertificateAuthority(CertificateAuthority):
"""
Generate certificate from CSR PEM Object.
This method set default certificate extensions, later will allow to set custom extensions
ca_key_pair: ca_key_pair which should be used to sign certificate
req: csr object to sign
serial: serial to apply to the new signed certificate
subject: give a dict containing new subject to apply on signed certificate
Backup-able CertificateAuthority.
if subject is None, req.get_subject() is used.
See backup schema in documentation.
"""
if subject is None:
subject = req.get_subject()
# Here comes the actual certificate
cert = crypto.X509()
# version v3
cert.set_version(2)
cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(self.crt_life_time)
cert.set_issuer(ca_key_pair['crt'].get_subject())
cert.set_subject(subject)
cert.set_pubkey(req.get_pubkey())
self.extension_manager.setDefaultExtensions(
cert,
subject=cert,
issuer=ca_key_pair['crt'],
crl_url=self.crl_base_url)
cert.sign(ca_key_pair['key'], self.default_digest)
return cert
def _createCertificateRevocationList(self):
def doBackup(self, write):
"""
Create CRL from certification revocation_list and return a PEM string content
Backup the entire storage to given path, enciphering it using all stored
certificates.
"""
revocation_list = self._storage.getRevocationList()
now = datetime.utcnow()
crl = crypto.CRL()
# XXX - set_nextUpdate() doesn't update Next Update in generated CRL,
# So we have used export() which takes the number of days in param
#
# next_date = now + timedelta(0, 864000) #self.crl_life_time)
# crl.set_lastUpdate(now.strftime("%Y%m%d%H%M%SZ").encode("ascii"))
# crl.set_nextUpdate(next_date.strftime("%Y%m%d%H%M%SZ").encode("ascii"))
num_crl_days = int(round(self.crl_life_time/(24.0*60*60), 0))
if num_crl_days == 0:
# At least one day
num_crl_days = 1
for revocation in revocation_list:
revoked = crypto.Revoked()
revoked.set_rev_date(
revocation.creation_date.strftime("%Y%m%d%H%M%SZ").encode("ascii")
ca_cert_list = self.getCACertificateList()
crl = x509.load_pem_x509_crl(
self.getCertificateRevocationList(),
_cryptography_backend,
)
revoked.set_serial(revocation.serial.encode("ascii"))
revoked.set_reason(None) #b'%s' % revocation.reason)
crl.add_revoked(revoked)
version_number = self._storage.getNextCRLVersionNumber()
crl.set_version(version_number)
# XXX - set how to get the cacert here
cert = self._ca_key_pairs_list[-1]['crt']
key = self._ca_key_pairs_list[-1]['key']
#crl.sign(cert, key, self.default_digest)
dumped_crl = crl.export(
cert,
key,
type=crypto.FILETYPE_PEM,
days=num_crl_days,
digest=self.default_digest)
return self._storage.storeCertificateRevocationList(
crypto.load_crl(crypto.FILETYPE_PEM, dumped_crl),
expiration_date=(now + timedelta(num_crl_days, 0))
signing_key = os.urandom(32)
symetric_key = os.urandom(32)
iv = os.urandom(16)
encryptor = Cipher(
algorithms.AES(symetric_key),
modes.CBC(iv),
backend=_cryptography_backend,
).encryptor()
authenticator = hmac.HMAC(
signing_key,
hashes.SHA256(),
backend=_cryptography_backend,
)
HMAC_PAYLOAD_SIZE = 10 * 1024 * 1024
key_list = []
for crt_pem in self._storage.iterCertificates():
try:
crt = utils.load_certificate(crt_pem, ca_cert_list, crl)
except CertificateVerificationError:
continue
public_key = crt.public_key()
key_list.append({
'id': x509.SubjectKeyIdentifier.from_public_key(
public_key,
).digest.encode('hex'),
'cipher': {
'name': 'rsa_oaep_sha1_mgf1_sha1',
},
'key': public_key.encrypt(
signing_key + symetric_key,
OAEP(
mgf=MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None,
),
).encode('hex'),
})
if not key_list:
# No users yet, backup is meaningless
return False
header = json.dumps({
'cipher': {
'name': 'aes256_cbc_pkcs7_hmac_10M_sha256',
'parameter': iv.encode('hex'),
},
'key_list': key_list,
})
padder = padding.PKCS7(128).padder()
write(_BACKUP_MAGIC)
write(struct.pack('<I', len(header)))
write(header)
def signingIterator():
"""
Iterate over cleartext dump, inserting HMAC between each chunk.
"""
buf = b''
for chunk in self._storage.dumpIterator():
buf += chunk
while len(buf) >= HMAC_PAYLOAD_SIZE:
chunk = buf[:HMAC_PAYLOAD_SIZE]
buf = buf[HMAC_PAYLOAD_SIZE:]
authenticator.update(chunk)
yield chunk
yield authenticator.copy().finalize()
if buf:
authenticator.update(buf)
yield buf
yield authenticator.finalize()
for chunk in signingIterator():
write(encryptor.update(padder.update(chunk)))
write(encryptor.update(padder.finalize()))
write(encryptor.finalize())
return True
@classmethod
def restoreBackup(
cls,
db_class,
db_path,
read,
key_pem,
csr_pem,
db_kw=(),
kw=(),
):
"""
Restore backup, revoke certificate corresponding to private key and sign
its renewal.
"""
magic = read(8)
if magic != _BACKUP_MAGIC:
raise ValueError('Invalid backup magic string')
header_len, = struct.unpack(
'<I',
read(struct.calcsize('<I')),
)
header = json.loads(read(header_len))
if header['cipher']['name'] != 'aes256_cbc_pkcs7_hmac_10M_sha256':
raise ValueError('Unrecognised symetric cipher')
private_key = utils.load_privatekey(key_pem)
key_id = x509.SubjectKeyIdentifier.from_public_key(
private_key.public_key(),
).digest.encode('hex')
symetric_key_list = [
x for x in header['key_list'] if x['id'] == key_id
]
if not symetric_key_list:
raise ValueError(
'Given private key is not a good candidate for restoring this backup',
)
symetric_key_entry, = symetric_key_list
if symetric_key_entry['cipher']['name'] != 'rsa_oaep_sha1_mgf1_sha1':
raise ValueError('Unrecognised asymetric cipher')
both_keys = private_key.decrypt(
symetric_key_entry['key'].decode('hex'),
OAEP(
mgf=MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None,
),
)
if len(both_keys) != 64:
raise ValueError('Invalid key length')
decryptor = Cipher(
algorithms.AES(both_keys[32:]),
modes.CBC(header['cipher']['parameter'].decode('hex')),
backend=_cryptography_backend,
).decryptor()
unpadder = padding.PKCS7(128).unpadder()
authenticator = hmac.HMAC(
both_keys[:32],
hashes.SHA256(),
backend=_cryptography_backend,
)
HMAC_PAYLOAD_SIZE = 10 * 1024 * 1024
# Each block has its signature
HMAC_SIGNED_SIZE = HMAC_PAYLOAD_SIZE + 32
CHUNK_SIZE = HMAC_SIGNED_SIZE
def restorator():
"""
Iterate over backup payload, decyphering by small chunks.
"""
while True:
chunk = read(CHUNK_SIZE)
if chunk:
yield unpadder.update(decryptor.update(chunk))
else:
yield unpadder.update(decryptor.finalize()) + unpadder.finalize()
break
def verificator():
"""
Iterate over decrypted payload, verifying HMAC on each chunk.
"""
buf = b''
for clear in restorator():
buf += clear
while len(buf) >= HMAC_SIGNED_SIZE:
signed = buf[:HMAC_SIGNED_SIZE]
buf = buf[HMAC_SIGNED_SIZE:]
chunk = signed[:-32]
authenticator.update(chunk)
authenticator.copy().verify(signed[-32:])
yield chunk
if buf:
chunk = buf[:-32]
authenticator.update(chunk)
authenticator.verify(buf[-32:])
yield chunk
db_class.restore(db_path=db_path, restorator=verificator())
# Now that the database is restored, use a CertificateAuthority to
# renew & revoke given private key.
self = cls(storage=db_class(db_path=db_path, **dict(db_kw)), **dict(kw))
# pylint: disable=protected-access
crt_pem = self._storage.getCertificateByKeyIdentifier(key_id)
# pylint: enable=protected-access
new_crt_pem = self.renew(crt_pem, csr_pem)
self.revoke(crt_pem)
return new_crt_pem
......@@ -15,33 +15,862 @@
#
# You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
from __future__ import absolute_import
import argparse
import datetime
import httplib
import json
import os
from caucase import app, db
from flask_alchemydumps import AlchemyDumps, AlchemyDumpsCommand
from flask_script import Manager, Command
import struct
import sys
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from . import utils
from . import exceptions
from .client import (
CaucaseError,
CaucaseClient,
HTTPSOnlyCaucaseClient,
updateCAFile,
updateCRLFile,
)
_cryptography_backend = default_backend()
app.config.update(
DEBUG=False,
CSRF_ENABLED=True,
TESTING=False,
SQLALCHEMY_DATABASE_URI='sqlite:///%sca.db' % os.environ.get('CAUCASE_DIR', '')
)
STATUS_ERROR = 1
STATUS_WARNING = 2
STATUS_CALLBACK_ERROR = 3
MODE_SERVICE = 'service'
MODE_USER = 'user'
class CLICaucaseClient(object):
"""
CLI functionalities
"""
# Note: this class it more to reduce local variable scopes (avoiding
# accidental mixups) in each methods than about API declaration.
def __init__(self, client):
self._client = client
def putCSR(self, csr_path_list):
"""
--send-csr
"""
for csr_path in csr_path_list:
csr_pem = utils.getCertRequest(csr_path)
# Quick sanity check
utils.load_certificate_request(csr_pem)
print self._client.putCSR(csr_pem), csr_path
def getCSR(self, csr_id_path_list):
"""
--get-csr
"""
for csr_id, csr_path in csr_id_path_list:
csr_pem = self._client.getCSR(int(csr_id))
with open(csr_path, 'a') as csr_file:
csr_file.write(csr_pem)
def getCRT(self, warning, error, crt_id_path_list, ca_list):
"""
--get-crt
"""
for crt_id, crt_path in crt_id_path_list:
crt_id = int(crt_id)
try:
crt_pem = self._client.getCRT(crt_id)
except CaucaseError, e:
if e.args[0] != httplib.NOT_FOUND:
raise
try:
self._client.getCSR(crt_id)
except CaucaseError, e:
if e.args[0] != httplib.NOT_FOUND:
raise
print crt_id, 'not found - either csr id has a typo or CSR was rejected'
error = True
else:
print crt_id, 'CSR still pending'
warning = True
else:
print crt_id,
if utils.isCertificateAutoSigned(utils.load_certificate(
crt_pem,
ca_list,
None,
)):
print 'was (originally) automatically approved'
else:
print 'was (originally) manually approved'
if os.path.exists(crt_path):
try:
key_pem = utils.getKey(crt_path)
except ValueError:
print >>sys.stderr, (
'Expected to find exactly one privatekey key in %s, skipping' % (
crt_path,
)
)
error = True
continue
try:
utils.validateCertAndKey(crt_pem, key_pem)
except ValueError:
print >>sys.stderr, (
'Key in %s does not match retrieved certificate, skipping'
)
error = True
continue
with open(crt_path, 'a') as crt_file:
crt_file.write(crt_pem)
return warning, error
def revokeCRT(self, error, crt_key_list):
"""
--revoke-crt
"""
for crt_path, key_path in crt_key_list:
try:
crt, key, _ = utils.getKeyPair(crt_path, key_path)
except ValueError:
print >>sys.stderr, (
'Could not find (exactly) one matching key pair in %s, skipping' % (
[x for x in set((crt_path, key_path)) if x]
)
)
error = True
continue
self._client.revokeCRT(crt, key)
return error
def renewCRT(
self,
crt_key_list,
renewal_deadline,
key_len,
ca_certificate_list,
updated,
error,
):
"""
--renew-crt
"""
for crt_path, key_path in crt_key_list:
try:
old_crt_pem, old_key_pem, key_path = utils.getKeyPair(
crt_path,
key_path,
)
except ValueError:
print >>sys.stderr, (
'Could not find (exactly) one matching key pair in %s, skipping' % (
[x for x in set((crt_path, key_path)) if x]
)
)
error = True
continue
try:
old_crt = utils.load_certificate(old_crt_pem, ca_certificate_list, None)
except exceptions.CertificateVerificationError:
print crt_path, (
'was not signed by this CA, revoked or otherwise invalid, skipping'
)
continue
if renewal_deadline < old_crt.not_valid_after:
print crt_path, 'did not reach renew threshold, not renewing'
continue
new_key_pem, new_crt_pem = self._client.renewCRT(
old_crt=old_crt,
old_key=utils.load_privatekey(old_key_pem),
key_len=key_len,
)
if key_path is None:
with open(crt_path, 'w') as crt_file:
crt_file.write(new_key_pem)
crt_file.write(new_crt_pem)
else:
with open(crt_path, 'w') as crt_file, open(key_path, 'w') as key_file:
key_file.write(new_key_pem)
crt_file.write(new_crt_pem)
updated = True
return updated, error
manager = Manager(app)
def listCSR(self, mode):
"""
--list-csr
"""
print '-- pending', mode, 'CSRs --'
print '%20s | %s' % (
'csr_id',
'subject preview (fetch csr and check full content !)',
)
for entry in self._client.getCSRList():
csr = utils.load_certificate_request(entry['csr'])
print '%20i | %r' % (
entry['id'],
csr.subject,
)
print '-- end of pending', mode, 'CSRs --'
# init Alchemy Dumps
alchemydumps = AlchemyDumps(app, db)
manager.add_command('database', AlchemyDumpsCommand)
def signCSR(self, csr_id_list):
"""
--sign-csr
"""
for csr_id in csr_id_list:
self._client.signCSR(int(csr_id))
@manager.command
def housekeep():
def signCSRWith(self, csr_id_path_list):
"""
Start Storage housekeep method to cleanup garbages
--sign-csr-with
"""
from caucase.storage import Storage
storage = Storage(db)
storage.housekeep()
for csr_id, csr_path in csr_id_path_list:
self._client.signCSR(
int(csr_id),
template_csr=utils.getCertRequest(csr_path),
)
def main():
manager.run()
def rejectCSR(self, csr_id_list):
"""
--reject-csr
"""
for csr_id in csr_id_list:
self._client.deleteCSR(int(csr_id))
def revokeOtherCRT(self, crt_list):
"""
--revoke-other-crt
"""
error = False
for crt_path in crt_list:
try:
# Note: also raises when there are serveral certs. This is intended:
# revoking many certs from a single file seems a dubious use-case
# (especially in the automated issuance context, which is supposed to
# be caucase's main target), with high risk if carried without
# questions (too many certificates revoked, or asingle unexpected one
# among these, ...) and unambiguous solution is easy (if a human is
# involved, as is likely the case, more or less directly, for
# authenticated revocations).
crt_pem = utils.getCert(crt_path)
except ValueError:
print >>sys.stderr, (
'Could not load a single certificate in %s, skipping' % (
crt_path,
)
)
self._client.revokeCRT(crt_pem)
return error
def revokeSerial(self, serial_list):
"""
--revoke-serial
"""
for serial in serial_list:
self._client.revokeSerial(serial)
def main(argv=None):
"""
Command line caucase client entry point.
"""
parser = argparse.ArgumentParser(description='caucase')
# XXX: currently, it is the server which chooses which digest is used to sign stuff.
# Should clients be able to tell it how to sign (and server could reject) ?
parser.add_argument(
'--ca-url',
required=True,
metavar='URL',
help='caucase service HTTP base URL.',
)
parser.add_argument(
'--ca-crt',
default='cas.crt.pem',
metavar='CRT_PATH',
help='Services CA certificate file location. default: %(default)s',
)
parser.add_argument(
'--user-ca-crt',
default='cau.crt.pem',
metavar='CRT_PATH',
help='Users CA certificate file location. default: %(default)s',
)
parser.add_argument(
'--crl',
default='cas.crl.pem',
metavar='CRL_PATH',
help='Services certificate revocation list location. default: %(default)s',
)
parser.add_argument(
'--user-crl',
default='cau.crl.pem',
metavar='CRL_PATH',
help='Users certificate revocation list location. default: %(default)s',
)
parser.add_argument(
'--threshold',
default=31,
type=float,
help='The remaining certificate validity period, in days, under '
'which a renew is desired. default: %(default)s',
)
parser.add_argument(
'--key-len',
default=2048,
type=int,
metavar='BITLENGTH',
help='Number of bits to use when generating a new private key. '
'default: %(default)s',
)
parser.add_argument(
'--on-renew',
metavar='EXECUTABLE_PATH',
help='Path of an executable file to call after any renewal (CA cert, '
'certificate, revocation list).',
)
parser.add_argument(
'--user-key',
metavar='KEY_PATH',
help='User private key and certificate bundled in a single file to '
'authenticate with caucase.',
)
parser.add_argument(
'--mode',
default=MODE_SERVICE,
choices=[MODE_SERVICE, MODE_USER],
help='The type of certificates you want to manipulate: '
'<%s> certificates allow managing caucase server, '
'<%s> certificates can be validated by caucase\'s CA certificate. '
'default: %%(default)s' % (
MODE_USER,
MODE_SERVICE,
)
)
anonymous_group = parser.add_argument_group(
'Anonymous actions',
'Actions which do no require authentication.',
)
anonymous_group.add_argument(
'--send-csr',
nargs='+',
metavar='CSR_PATH',
default=[],
help='Request signature of these certificate signing requests.',
)
anonymous_group.add_argument(
'--get-crt',
nargs=2,
action='append',
default=[],
metavar=('CSR_ID', 'CRL_PATH'),
help='Retrieve the certificate identified by '
'identifier and store it at given path. '
'If CSR_PATH exists and contains the private key corresponding to '
'received certificate, certificate will be appended to that file. '
'Can be given multiple times.',
)
anonymous_group.add_argument(
'--revoke-crt',
nargs=2,
action='append',
default=[],
metavar=('CRT_PATH', 'KEY_PATH'),
help='Revoke certificate. If CRT_PATH file contains both a certificate '
'and a key, KEY_PATH is ignored. '
'Can be given multiple times.',
)
anonymous_group.add_argument(
'--renew-crt',
nargs=2,
default=[],
action='append',
metavar=('CRT_PATH', 'KEY_PATH'),
help='Renew certificates in-place if they exceed THRESHOLD. '
'If CRT_PATH file contains both certificate and key, KEY_PATH is ignored '
'and CRT_PATH receives both the new key and the new certificate. '
'Can be given multiple times.',
)
anonymous_group.add_argument(
'--get-csr',
nargs=2,
default=[],
action='append',
metavar=('CSR_ID', 'CSR_PATH'),
help='Retrieve certificate signing request and append to CSR_PATH. '
'Should only be needed before deciding to sign or reject the request. '
'Can be given multiple times.',
)
anonymous_group.add_argument(
'--update-user',
action='store_true',
help='Update or create user CA and CRL. '
'Should only be needed by the https server in front of caucase.'
)
authenticated_group = parser.add_argument_group(
'Authenticated actions',
'Actions which require an authentication. Requires --user-key .',
)
authenticated_group.add_argument(
'--list-csr',
action='store_true',
help='List certificate signing requests currently pending on server.',
)
authenticated_group.add_argument(
'--sign-csr',
nargs='+',
default=[],
metavar='CSR_ID',
help='Sign pending certificate signing requests.',
)
authenticated_group.add_argument(
'--sign-csr-with',
nargs=2,
default=[],
action='append',
metavar=('CSR_ID', 'CSR_PATH'),
help='Sign pending certificate signing request, but use provided CSR for '
'requested subject and extensions instead of stored CSR. '
'Can be given multiple times.',
)
authenticated_group.add_argument(
'--reject-csr',
nargs='+',
default=[],
metavar='CSR_ID',
help='Reject these pending certificate signing requests.',
)
authenticated_group.add_argument(
'--revoke-other-crt',
nargs='+',
default=[],
metavar='CRT_PATH',
help='Revoke certificate without needing access to their private key.'
)
authenticated_group.add_argument(
'--revoke-serial',
nargs='+',
default=[],
metavar='SERIAL',
type=int,
help='Revoke certificate by serial number, without needing the '
'certificate at all. DANGEROUS: typos will not be detected ! '
'COSTLY: revocation will stay in the revocation list until all '
'currently valid CA certificates have expired. '
'Use --revoke and --revoke-other-crt whenever possible.',
)
args = parser.parse_args(argv)
sign_csr_id_set = set(args.sign_csr)
sign_with_csr_id_set = {x for x, _ in args.sign_csr_with}
if (
sign_csr_id_set.intersection(args.reject_csr) or
sign_with_csr_id_set.intersection(args.reject_csr) or
sign_csr_id_set.intersection(sign_with_csr_id_set)
):
print >>sys.stderr, (
'A given CSR_ID cannot be in more than one of --sign-csr, '
'--sign-csr-with and --reject-csr'
)
raise SystemExit(STATUS_ERROR)
updated = False
warning = False
error = False
finished = False
cau_url = args.ca_url + '/cau'
cas_url = args.ca_url + '/cas'
try:
# Get a working, up-to-date CAS CA certificate file.
updated |= updateCAFile(cas_url, args.ca_crt)
# --update-user, CA part
if args.update_user or args.mode == MODE_USER:
updated |= updateCAFile(cau_url, args.user_ca_crt)
client = CLICaucaseClient(
client=CaucaseClient(
ca_url={
MODE_SERVICE: cas_url,
MODE_USER: cau_url,
}[args.mode],
ca_crt_pem_list=utils.getCertList(args.ca_crt),
user_key=args.user_key,
),
)
ca_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList({
MODE_SERVICE: args.ca_crt,
MODE_USER: args.user_ca_crt,
}[args.mode])
]
client.putCSR(args.send_csr)
client.getCSR(args.get_csr)
warning, error = client.getCRT(warning, error, args.get_crt, ca_list)
error = client.revokeCRT(error, args.revoke_crt)
updated, error = client.renewCRT(
crt_key_list=args.renew_crt,
renewal_deadline=datetime.datetime.utcnow() + datetime.timedelta(
args.threshold,
0,
),
key_len=args.key_len,
ca_certificate_list=ca_list,
updated=updated,
error=error,
)
client.signCSR(args.sign_csr)
client.signCSRWith(args.sign_csr_with)
client.rejectCSR(args.reject_csr)
error |= client.revokeOtherCRT(args.revoke_other_crt)
client.revokeSerial(args.revoke_serial)
# show latest CSR list status
if args.list_csr:
client.listCSR(args.mode)
# update our CRL after all revocations we were requested
updated |= updateCRLFile(cas_url, args.crl, [
utils.load_ca_certificate(x)
for x in utils.getCertList(args.ca_crt)
])
# --update-user, CRL part
if args.update_user:
updated |= updateCRLFile(cau_url, args.user_crl, [
utils.load_ca_certificate(x)
for x in utils.getCertList(args.user_ca_crt)
])
finished = True
finally:
if updated and args.on_renew:
status = os.system(args.on_renew)
# Avoid raising if we arrived here because of an exception, to not hide
# the original problem.
if finished and status:
raise SystemExit(STATUS_CALLBACK_ERROR)
if error:
raise SystemExit(STATUS_ERROR)
if warning:
raise SystemExit(STATUS_WARNING)
def probe(argv=None):
"""
Verify basic caucase server functionality
"""
parser = argparse.ArgumentParser(
description='caucase probe - Verify basic caucase server functionality',
)
parser.add_argument(
'ca_url',
nargs=1,
help='caucase service HTTP base URL.',
)
ca_url, = parser.parse_args(argv).ca_url
cas_url = ca_url + '/cas'
http_client = CaucaseClient(
ca_url=cas_url,
)
http_ca_pem = http_client.getCA()
https_ca_pem = HTTPSOnlyCaucaseClient(
ca_url=cas_url,
ca_crt_pem_list=[http_ca_pem],
).getCA()
# Retrieve again in case there was a renewal between both calls - we do
# not expect 2 renewals in very short succession.
http2_ca_pem = http_client.getCA()
if https_ca_pem not in (http_ca_pem, http2_ca_pem):
raise ValueError('http and https do not serve the same caucase database')
def monitor(argv=None):
"""
Bootstrap certificate and companion files and keep them up-to-date.
"""
parser = argparse.ArgumentParser(
description='caucase monitor - '
'Bootstrap certificate and companion files and keep them up-to-date',
)
parser.add_argument(
'--ca-url',
required=True,
metavar='URL',
help='caucase service HTTP base URL.',
)
parser.add_argument(
'--cas-ca',
required=True,
metavar='CRT_PATH',
help='Service CA certificate file location used to verify connection '
'to caucase. Will be maintained up-to-date. '
'default: %(default)s',
)
parser.add_argument(
'--threshold',
default=31,
type=float,
help='The remaining certificate validity period, in days, under '
'which a renew is desired. default: %(default)s',
)
parser.add_argument(
'--key-len',
default=2048,
type=int,
metavar='BITLENGTH',
help='Number of bits to use when generating a new private key. '
'default: %(default)s',
)
parser.add_argument(
'--on-renew',
metavar='EXECUTABLE_PATH',
help='Path of an executable file to call after any renewal (CA cert, '
'certificate, revocation list).',
)
parser.add_argument(
'--max-sleep',
default=31,
type=float,
help='Maximum number of days to sleep for. Allows refreshing the CRL '
'more often on sensitive services. default: %(default)s',
)
parser.add_argument(
'--mode',
default=MODE_SERVICE,
choices=[MODE_SERVICE, MODE_USER],
help='The type of certificates you want to manipulate: '
'<%s> certificates allow managing caucase server, '
'<%s> certificates can be validated by caucase\'s CA certificate. '
'default: %%(default)s' % (
MODE_USER,
MODE_SERVICE,
)
)
parser.add_argument(
'--csr',
metavar='CSR_PATH',
help='Path of your CSR to use for initial request of a certificate for '
'MODE. Ignored once a certificate exists at the location given by '
'--crt .',
)
parser.add_argument(
'--key',
metavar='KEY_PATH',
help='Path of your private key file. Must always exist when this command '
'is started. Will be updated on certificate renewal. If not provided, both '
'key and certificate will be stored in the file pointed at by --crt .',
)
parser.add_argument(
'--crt',
required=True,
metavar='CRT_PATH',
help='Path of your certificate for MODE. Will be renewed before '
'expiration.',
)
parser.add_argument(
'--ca',
required=True,
metavar='CRT_PATH',
help='Path of your CA certificate for MODE. '
'Will be maintained up-to-date.'
)
parser.add_argument(
'--crl',
required=True,
metavar='CRT_PATH',
help='Path of your certificate revocation list for MODE. '
'Will be maintained up-to-date.'
)
args = parser.parse_args(argv)
try:
cas_url = args.ca_url + '/cas'
ca_url = {
MODE_SERVICE: cas_url,
MODE_USER: args.ca_url + '/cau',
}[args.mode]
threshold = datetime.timedelta(args.threshold, 0)
max_sleep = datetime.timedelta(args.max_sleep, 0)
updated = updateCAFile(cas_url, args.cas_ca) and args.cas_ca == args.ca
client = CaucaseClient(
ca_url=ca_url,
ca_crt_pem_list=utils.getCertList(args.cas_ca)
)
if not utils.hasOneCert(args.crt):
print 'Bootstraping...'
csr_pem = utils.getCertRequest(args.csr)
# Quick sanity check before bothering server
utils.load_certificate_request(csr_pem)
csr_id = client.putCSR(csr_pem)
print 'Waiting for signature of', csr_id
while True:
try:
crt_pem = client.getCRT(csr_id)
except CaucaseError, e:
if e.args[0] != httplib.NOT_FOUND:
raise
# If server does not know our CSR anymore, getCSR will raise.
# If it does, we were likely rejected, so exit by letting exception
# through.
client.getCSR(csr_id)
# Still here ? Ok, wait a bit and try again.
utils.interruptibleSleep(60)
else:
with open(args.crt, 'a') as crt_file:
crt_file.write(crt_pem)
updated = True
break
print 'Bootstrap done'
next_deadline = datetime.datetime.utcnow()
while True:
print 'Next wake-up at', next_deadline.strftime(
'%Y-%m-%d %H:%M:%S +0000'
)
now = utils.until(next_deadline)
if args.cas_ca != args.ca and updateCAFile(cas_url, args.cas_ca):
client = CaucaseClient(
ca_url=ca_url,
ca_crt_pem_list=utils.getCertList(args.cas_ca)
)
if updateCAFile(ca_url, args.ca):
print 'Got new CA'
updated = True
ca_crt_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(args.ca)
]
if updateCRLFile(ca_url, args.crl, ca_crt_list):
print 'Got new CRL'
updated = True
crt_pem, key_pem, key_path = utils.getKeyPair(args.crt, args.key)
crt = utils.load_certificate(crt_pem, ca_crt_list, None)
next_deadline = crt.not_valid_after - threshold
if next_deadline <= now:
print 'Renewing', args.crt
new_key_pem, new_crt_pem = client.renewCRT(
old_crt=crt,
old_key=utils.load_privatekey(key_pem),
key_len=args.key_len,
)
if key_path is None:
with open(args.crt, 'w') as crt_file:
crt_file.write(new_key_pem)
crt_file.write(new_crt_pem)
else:
with open(args.crt, 'w') as crt_file, open(key_path, 'w') as key_file:
key_file.write(new_key_pem)
crt_file.write(new_crt_pem)
updated = True
next_deadline = min(
next_deadline,
utils.load_crl(open(args.crl).read(), ca_crt_list).next_update,
now + max_sleep,
)
if updated:
if args.on_renew is not None:
status = os.system(args.on_renew)
if status:
print >>sys.stderr, 'Renewal hook exited with status:', status
raise SystemExit(STATUS_ERROR)
updated = False
except (utils.SleepInterrupt, SystemExit):
# Not intercepting KeyboardInterrupt so interrupting outside of
# interruptibleSleep shows where the script got interrupted.
pass
def rerequest(argv=None):
"""
Produce a new private key and sign a CSR created by copying an existing,
well-signed CSR.
"""
parser = argparse.ArgumentParser(
description='caucase rerequest - '
'Produce a new private key and sign a CSR created by copying an existing, '
'well-signed CSR.',
)
parser.add_argument(
'--template',
required=True,
type=argparse.FileType('r'),
help='Existing PEM-encodd CSR to use as a template.',
)
parser.add_argument(
'--csr',
required=True,
help='Path of produced PEM-encoded CSR.',
)
parser.add_argument(
'--key',
required=True,
help='Path of produced PEM-encoded private key.',
)
parser.add_argument(
'--key-len',
default=2048,
type=int,
metavar='BITLENGTH',
help='Number of bits to use when generating a new private key. '
'default: %(default)s',
)
args = parser.parse_args(argv)
template = utils.load_certificate_request(utils.getCertRequest(args.template))
key = utils.generatePrivateKey(key_len=args.key_len)
csr_pem = utils.dump_certificate_request(
x509.CertificateSigningRequestBuilder(
subject_name=template.subject,
extensions=template.extensions,
).sign(
private_key=key,
algorithm=utils.DEFAULT_DIGEST_CLASS(),
backend=_cryptography_backend,
),
)
key_pem = utils.dump_privatekey(key)
orig_umask = os.umask(0177)
try:
with open(args.key, 'w') as key_file:
key_file.write(key_pem)
finally:
os.umask(orig_umask)
with open(args.csr, 'w') as csr_file:
csr_file.write(csr_pem)
def key_id(argv=None):
"""
Displays key identifier from private key, and the list of acceptable key
identifiers for a given backup file.
"""
parser = argparse.ArgumentParser(
description='caucase key id - '
'Displays key identifier from private key, and the list of acceptable key'
'identifiers for a given backup file.',
)
parser.add_argument(
'--private-key',
nargs='+',
default=(),
help='PEM-encoded keys to display the identifier of.',
)
parser.add_argument(
'--backup',
nargs='+',
default=(),
help='Caucase backup files to display acceptable deciphering key '
'identifiers of.',
)
args = parser.parse_args(argv)
for key_path in args.private_key:
print key_path, x509.SubjectKeyIdentifier.from_public_key(
utils.load_privatekey(open(key_path).read()).public_key(),
).digest.encode('hex')
for backup_path in args.backup:
print backup_path
with open(backup_path) as backup_file:
magic = backup_file.read(8)
if magic != 'caucase\0':
raise ValueError('Invalid backup magic string')
header_len, = struct.unpack(
'<I',
backup_file.read(struct.calcsize('<I')),
)
for key_entry in json.loads(backup_file.read(header_len))['key_list']:
print ' ', key_entry['id']
# 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 os, errno
import time
import ConfigParser
import logging
import requests
import argparse
import traceback
import pem
import json
import subprocess
import hashlib
from OpenSSL import crypto
from caucase import utils
from datetime import datetime, timedelta
CSR_KEY_FILE = 'csr.key.txt'
RENEW_CSR_KEY_FILE = 'renew_csr.key.txt'
def popenCommunicate(command_list):
subprocess_kw = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
popen = subprocess.Popen(command_list, **subprocess_kw)
result = popen.communicate()[0]
if popen.returncode is None:
popen.kill()
if popen.returncode != 0:
raise ValueError('Issue during calling %r, result was:\n%s' % (
command_list, result))
return result
def parseArguments():
"""
Parse arguments for Certificate Authority Request.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--ca-url',
required=True,
help='Certificate Authority URL')
parser.add_argument('-c', '--ca-crt-file',
default='ca.crt.pem',
help='Path for CA Cert file. default: %(default)s')
parser.add_argument('-x', '--crt-file',
default='crt.pem',
help='Path for Certificate file. default: %(default)s')
parser.add_argument('-k', '--key-file',
default='key.pem',
help='Path of key file. default: %(default)s')
parser.add_argument('-s', '--csr-file',
default='csr.pem',
help='Path where to store csr file. default: %(default)s')
parser.add_argument('-r', '--crl-file',
default='crl.pem',
help='Path where to store crl file. default: %(default)s')
parser.add_argument('--digest',
default="sha256",
help='Digest used to sign data. default: %(default)s')
parser.add_argument('--cn',
help='Common name to use when request new certificate.')
parser.add_argument('--threshold',
help='The minimum remaining certificate validity time in' \
' seconds after which renew of certificate can be triggered.',
type=int)
parser.add_argument('--on-renew',
help='Path of an executable file to call after certificate'\
' renewal.')
parser.add_argument('--on-crl-update',
help='Path of an executable file to call after CRL '\
'file update.')
parser.add_argument('--no-check-certificate',
action='store_false', default=True, dest='verify_certificate',
help='When connecting to CA on HTTPS, disable certificate verification.')
group = parser.add_mutually_exclusive_group()
group.add_argument('--request', action='store_true',
help='Request a new Certificate.')
group.add_argument('--revoke', action='store_true',
help='Revoke existing certificate.')
group.add_argument('--renew', action='store_true',
help='Renew current certificate and and replace with existing files.')
group.add_argument('--update-crl', action='store_true',
help='Download and store the new CRL file if there was a new revocation.')
return parser
def requestCertificate(ca_request, config):
# download or update ca crt file
ca_request.getCACertificateChain()
if os.path.exists(config.crt_file):
return
if not os.path.exists(config.csr_file):
csr = ca_request.generateCertificateRequest(config.key_file,
cn=config.cn, csr_file=config.csr_file)
else:
csr = open(config.csr_file).read()
ca_request.signCertificate(csr)
def revokeCertificate(ca_revoke, config):
os.close(os.open(config.key_file, os.O_RDONLY))
os.close(os.open(config.crt_file, os.O_RDONLY))
# download or update ca crt file
ca_revoke.getCACertificateChain()
ca_revoke.revokeCertificate()
def renewCertificate(ca_renew, config, backup_dir):
os.close(os.open(config.key_file, os.O_RDONLY))
os.close(os.open(config.crt_file, os.O_RDONLY))
# download or update ca crt file
ca_renew.getCACertificateChain()
ca_renew.renewCertificate(config.csr_file,
backup_dir,
config.threshold,
after_script=config.on_renew)
def main():
parser = parseArguments()
config = parser.parse_args()
base_dir = os.path.dirname(config.crt_file)
os.chdir(os.path.abspath(base_dir))
if not config.ca_url:
parser.error('`ca-url` parameter is required. Use --ca-url URL')
parser.print_help()
exit(1)
ca_client = CertificateAuthorityRequest(config.key_file, config.crt_file,
config.ca_crt_file, config.ca_url, digest=config.digest,
verify_certificate=config.verify_certificate)
if config.request:
if not config.cn:
parser.error('Option --cn is required for request.')
parser.print_help()
exit(1)
requestCertificate(ca_client, config)
elif config.revoke:
revokeCertificate(ca_client, config)
elif config.renew:
if not config.threshold:
parser.error('`threshold` parameter is required with renew. Use --threshold VALUE')
parser.print_help()
exit(1)
backup_dir = os.path.join('.',
'backup-%s' % datetime.now().strftime('%Y-%m-%d-%H%M%S'))
# cleanup
if os.path.exists(CSR_KEY_FILE):
os.unlink(CSR_KEY_FILE)
if os.path.exists(config.csr_file):
os.unlink(config.csr_file)
renewCertificate(ca_client, config, backup_dir)
elif config.update_crl:
ca_client.updateCertificateRevocationList(config.crl_file,
after_script=config.on_crl_update)
else:
parser.error('Please set one of options: --request | --revoke | --renew | --update-crl.')
parser.print_help()
exit(1)
class CertificateAuthorityRequest(object):
def __init__(self, key, certificate, cacertificate, ca_url,
max_retry=10, digest="sha256", sleep_time=5,
verify_certificate=False, logger=None):
self.key = key
self.certificate = certificate
self.cacertificate = cacertificate
self.ca_url = ca_url
self.logger = logger
# maximum retry number of post/put request
self.max_retry = max_retry
# time to sleep before retry failed request
self.sleep_time = sleep_time
self.digest = digest
self.extension_manager = utils.X509Extension()
self.ca_certificate_list = []
self.verify_certificate = verify_certificate
while self.ca_url.endswith('/'):
# remove all / at end or ca_url
self.ca_url = self.ca_url[:-1]
if os.path.exists(self.cacertificate):
self.ca_certificate_list = [
crypto.load_certificate(crypto.FILETYPE_PEM, x._pem_bytes) for x in
pem.parse_file(self.cacertificate)
]
if self.logger is None:
self.logger = logging.getLogger('Caucase Client')
self.logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.generatePrivatekey(self.key)
def _request(self, method, url, data=None):
try:
req = getattr(requests, method)
kw = {}
if data:
kw['data'] = data
kw['verify'] = self.verify_certificate
return req(url, **kw)
except requests.ConnectionError, e:
self.logger.error("Got ConnectionError while sending request to CA. Url is %s\n%s" % (
url, str(e)))
return None
def _checkCertEquals(self, first_cert, second_cert):
"""
Say if two certificate PEM object are the same
XXX - more checks should be done ?
"""
return first_cert.set_subject().CN == second_cert.get_subject().CN and \
first_cert.get_serial_number() == second_cert.get_serial_number()
def _writeNewFile(self, file_path, content, mode=0644):
fd = os.open(file_path,
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_TRUNC, mode)
try:
os.write(fd, content)
finally:
os.close(fd)
def _sendCertificateSigningRequest(self, csr_string):
request_url = '%s/csr' % self.ca_url
data = {'csr': csr_string}
retry = 0
response = self._request('put', request_url, data=data)
while (not response or response.status_code != 201) and retry < self.max_retry:
self.logger.error("%s: Failed to sent CSR. \n%s" % (
response.status_code, response.text))
self.logger.info("will retry in %s seconds..." % self.sleep_time)
time.sleep(self.sleep_time)
retry += 1
response = self._request('put', request_url, data=data)
if not response or response.status_code != 201:
raise Exception("ERROR: failed to send CSR after %s retry." % retry)
self.logger.info("CSR succefully sent.")
# Get csr Location from request header: http://xxx.com/csr/key
self.logger.debug("CSR location is: %s" % response.headers['Location'])
csr_key = response.headers['Location'].split('/')[-1]
with open(CSR_KEY_FILE, 'w') as fkey:
fkey.write(csr_key)
return csr_key
def _sendCertificateRenewal(self, cert, csr):
payload = dict(renew_csr=csr, crt=cert)
pkey = open(self.key).read()
wrapped = utils.wrap(payload, pkey, [self.digest])
request_url = '%s/crt/renew' % self.ca_url
data = {'payload': json.dumps(wrapped)}
self.logger.info("Sending Certificate Renewal request...")
response = self._request('put', request_url, data=data)
break_code = [201, 404, 500, 404]
retry = 1
while response is None or response.status_code not in break_code:
self.logger.error("%s: Failed to send renewal request. \n%s" % (
response.status_code, response.text))
self.logger.info("will retry in %s seconds..." % self.sleep_time)
time.sleep(self.sleep_time)
response = self._request('put', request_url, data=data)
retry += 1
if retry > self.max_retry:
break
if not response or response.status_code != 201:
raise Exception("ERROR: failed to send certificate renewal request "\
"after %s retry.\n%s" % (
retry, response.text))
csr_key = response.headers['Location'].split('/')[-1]
with open(RENEW_CSR_KEY_FILE, 'w') as fkey:
fkey.write(csr_key)
return csr_key
def _getSignedCertificate(self, crt_id):
reply_url = '%s/crt/%s' % (self.ca_url, crt_id)
response = self._request('get', reply_url)
while not response or response.status_code != 200:
time.sleep(self.sleep_time)
response = self._request('get', reply_url)
return response.text
def generateCertificateRequest(self, key_file, cn,
country='', state='', locality='', email='', organization='',
organization_unit='', csr_file=None):
"""
Generate certificate Signature request.
Parameter `cn` is mandatory
"""
with open(key_file) as fkey:
key = crypto.load_privatekey(crypto.FILETYPE_PEM, fkey.read())
req = crypto.X509Req()
subject = req.get_subject()
subject.CN = cn
if country:
subject.C = country
if state:
subject.ST = state
if locality:
subject.L = locality
if organization:
subject.O = organization
if organization_unit:
subject.OU = organization_unit
if email:
subject.emailAddress = email
req.set_pubkey(key)
self.extension_manager.setDefaultCsrExtensions(req)
req.sign(key, self.digest)
csr = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req)
if csr_file is not None:
with open(csr_file, 'w') as req_file:
req_file.write(csr)
os.chmod(csr_file, 0640)
return csr
def generatePrivatekey(self, output_file, size=2048):
"""
Generate private key into `output_file`
"""
try:
key_fd = os.open(output_file,
os.O_CREAT|os.O_WRONLY|os.O_EXCL|os.O_TRUNC,
0600)
except OSError, e:
if e.errno != errno.EEXIST:
raise
else:
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, size)
os.write(key_fd, crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
os.close(key_fd)
def checkCertificateValidity(self, cert):
"""
validate the certificate PEM string with the CA Certificate and private key
"""
cert_pem = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
pkey = open(self.key).read()
key_pem = crypto.load_privatekey(crypto.FILETYPE_PEM, pkey)
return utils.checkCertificateValidity(
self.ca_certificate_list,
cert_pem,
key_pem)
def isCertExpirationDateValid(self, x509, threshold):
"""
Return True if remaning certificate valid time is second is lower than
the threshold value
"""
expiration_date = datetime.strptime(
x509.get_notAfter(), '%Y%m%d%H%M%SZ'
)
now_date = datetime.utcnow()
limit_date = now_date + timedelta(0, threshold)
expire_in = expiration_date - limit_date
if expire_in.days > 0.0:
return True
return False
def updateCACertificateChain(self):
"""
Request to CA all valid certificates an update in to cacertificate file
@note: if the CA has more that one valid certificate, the cacertificate
file will be updated contain concatenated cert them like:
CA_1
CA_2
...
CA_N
"""
ca_cert_url = '%s/crt/ca.crt.json' % self.ca_url
self.logger.info("Updating CA certificate file from %s" % ca_cert_url)
cert_list = response_json = []
cert_list_chain = ""
response = self._request('get', ca_cert_url)
while not response or response.status_code != 200:
# sleep a bit then try again until ca cert is ready
time.sleep(10)
response = self._request('get', ca_cert_url)
response_json = json.loads(response.text)
if len(response_json) > 0:
iter_ca_cert = iter(response_json)
is_valid = False
payload = utils.unwrap(iter_ca_cert.next(), lambda x: x['old'], [self.digest])
# check that old certificate is known
old_x509 = crypto.load_certificate(crypto.FILETYPE_PEM, payload['old'])
for x509 in self.ca_certificate_list:
if self._checkCertEquals(x509, old_x509):
is_valid = True
if not is_valid:
# no local certificate matches
raise CertificateVerificationError("Updated CA Certificate chain could " \
"not be validated using local CA Certificate at %r. \nYou can " \
"try removing your local ca file if it was not updated for more " \
"that a year." % self.cacertificate)
# if not old_x509.has_expired():
# XXX - TODO: check if expired old_x509 can break certificate validation
cert_list.append(old_x509)
cert_list.append(
crypto.load_certificate(crypto.FILETYPE_PEM, payload['new'])
)
cert_list_chain = "%s\n%s" % (payload['old'], payload['new'])
for next_item in iter_ca_cert:
payload = utils.unwrap(next_item, lambda x: x['old'], [self.digest])
old_x509 = crypto.load_certificate(crypto.FILETYPE_PEM, payload['old'])
if self._checkCertEquals(cert_list[-1], old_x509):
cert_list.append(
crypto.load_certificate(crypto.FILETYPE_PEM, payload['new'])
)
cert_list_chain += "\n%s" % payload['new']
else:
raise CertificateVerificationError("Get CA Certificate chain " \
"retourned %s \n\nbut validation of data failed" % response_json)
# dump into file
if not cert_list:
# Nothing to do...
return
self.ca_certificate_list = cert_list
fd = os.open(self.cacertificate, os.O_CREAT|os.O_WRONLY, 0640)
try:
os.write(fd, cert_list_chain)
finally:
os.close(fd)
def getCACertificateChain(self):
"""
Get CA certificate file.
If it's the first download, get the latest valid certificate at ca.crt.pem
else, update current cacertificate with list of valid ca certificat chain
"""
# If cert file exists exist
if os.path.exists(self.cacertificate) and os.stat(self.cacertificate).st_size > 0:
# Get all valids CA certificate
return self.updateCACertificateChain()
ca_cert_url = '%s/crt/ca.crt.pem' % self.ca_url
self.logger.info("getting CA certificate file %s" % ca_cert_url)
response = None
while not response or response.status_code != 200:
response = self._request('get', ca_cert_url)
if response is not None:
try:
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, response.text)
except crypto.Error, e:
# XXX - we got a bad certificate, break here ?
traceback.print_exc()
response = None
else:
self.ca_certificate_list = [x509]
break
# sleep a bit then try again until ca cert is ready
time.sleep(10)
fd = os.open(self.cacertificate,
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_TRUNC, 0640)
try:
os.write(fd, response.text)
finally:
os.close(fd)
def signCertificate(self, csr):
"""
Send certificate signature request and wait until the certificate is
signed.
csr parameter is string in PEM format
"""
if os.path.exists(self.certificate) and os.stat(self.certificate).st_size > 0:
# exit because the certificate exists
return
csr_key = ""
self.logger.info("Request signed certificate from CA...")
if os.path.exists(CSR_KEY_FILE):
with open(CSR_KEY_FILE) as fkey:
csr_key = fkey.read()
if csr_key:
self.logger.info("Csr was already sent to CA, using csr : %s" % csr_key)
else:
csr_key = self._sendCertificateSigningRequest(csr)
self.logger.info("Waiting for signed certificate...")
# csr is xxx.csr.pem so cert is xxx.cert.pem
certificate = self._getSignedCertificate('%s.crt.pem' % csr_key[:-8])
self.logger.info("Validating signed certificate...")
if not self.checkCertificateValidity(certificate):
# certificate verification failed, should raise ?
self.logger.warn("Certificate validation failed.\n" \
"Please double check the signed certificate before use. Also consider" \
"revoke it and request a new signed certificate.")
fd = os.open(self.certificate,
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_TRUNC, 0644)
try:
os.write(fd, certificate)
finally:
os.close(fd)
self.logger.info("Certificate correctly saved at %s." % self.certificate)
def revokeCertificate(self, message=""):
"""
Revoke the current certificate on CA.
"""
retry = 1
pkey = open(self.key).read()
cert = open(self.certificate).read()
cert_pem = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
payload = dict(
reason=message,
revoke_crt=cert)
wrapped = utils.wrap(payload, pkey, [self.digest])
request_url = '%s/crt/revoke' % self.ca_url
data = {'payload': json.dumps(wrapped)}
self.logger.info("Sending Certificate revocation request of CN: %s." % (
cert_pem.get_subject().CN))
response = self._request('put', request_url, data=data)
break_code = [201, 404, 500, 404]
while response is None or response.status_code not in break_code:
self.logger.error("%s: Failed to send revoke request. \n%s" % (
response.status_code, response.text))
self.logger.info("will retry in %s seconds..." % self.sleep_time)
time.sleep(self.sleep_time)
response = self._request('put', request_url, data=data)
retry += 1
if retry > self.max_retry:
break
if not response or response.status_code != 201:
raise Exception("ERROR: failed to put revoke certificate after %s retry. Exiting..." % retry)
self.logger.info("Certificate %s was successfully revoked." % (
self.certificate))
def renewCertificate(self, csr_file, backup_dir, threshold, renew_key=True,
after_script=''):
"""
Renew the current certificate. Regenerate private key if renew_key is `True`
"""
new_key_path = '%s.renew' % self.key
new_cert_path = '%s.renew' % self.certificate
key_file = self.key
cert = open(self.certificate).read()
cert_pem = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
csr_key = ""
if self.isCertExpirationDateValid(cert_pem, threshold):
self.logger.info("Nothing to do, no need to renew the certificate.")
return
try:
if renew_key:
self.generatePrivatekey(new_key_path)
key_file = new_key_path
if os.path.exists(RENEW_CSR_KEY_FILE):
csr_key = open(RENEW_CSR_KEY_FILE).read()
if not csr_key:
if not os.path.exists(csr_file):
csr = self.generateCertificateRequest(key_file,
cn=cert_pem.get_subject().CN,
csr_file=csr_file)
else:
csr = open(csr_file).read()
csr_key = self._sendCertificateRenewal(cert, csr)
self.logger.info("Waiting for signed certificate...")
new_cert = self._getSignedCertificate('%s.crt.pem' % csr_key[:-8])
if not os.path.exists(backup_dir):
os.mkdir(backup_dir)
self._writeNewFile(new_cert_path, new_cert)
# change location of files
if renew_key:
os.rename(self.key,
os.path.join(backup_dir, os.path.basename(self.key)))
os.rename(new_key_path, self.key)
self.logger.info("Private correctly renewed at %s." % self.key)
os.rename(self.certificate,
os.path.join(backup_dir, os.path.basename(self.certificate)))
os.rename(new_cert_path, self.certificate)
self.logger.info("Validating signed certificate...")
if not self.checkCertificateValidity(new_cert):
# certificate verification failed, should raise ?
self.logger.warn("Certificate validation failed.\n" \
"Please double check the signed certificate before use. Also consider" \
"revoke it and request a new signed certificate.")
else:
self.logger.info("Certificate correctly renewed at %s." % self.certificate)
except:
raise
else:
for path in [csr_file, RENEW_CSR_KEY_FILE]:
if os.path.exists(path):
os.unlink(path)
if after_script:
output = popenCommunicate([os.path.realpath(after_script)])
self.logger.info("Successfully executed script '%s' with output:\n%s" % (
after_script, output))
finally:
for path in [new_cert_path, new_key_path]:
if os.path.exists(path):
os.unlink(path)
def updateCertificateRevocationList(self, crl_file, after_script=''):
"""
Download or update crl. If the crl_file exists, it will be updated if
the new CRL has changed.
"""
crl_url = '%s/crl' % self.ca_url
self.logger.info("Downloading crl file from %s ..." % crl_url)
response = self._request('get', crl_url)
retry = 1
while not response or response.status_code != 200:
time.sleep(self.sleep_time)
response = self._request('get', crl_url)
retry += 1
if retry > self.max_retry:
break
if not response or response.status_code != 200:
raise Exception("ERROR: failed to get crl file after %s retry. Exiting..." % retry)
crl_string = response.text
# load crl string so we are sure that it is a valid crl string
crl = crypto.load_crl(crypto.FILETYPE_PEM, crl_string)
# Dumped string contain only the CRL without extra info
crl_string = crypto.dump_crl(crypto.FILETYPE_PEM, crl)
update_crl = False
if os.path.exists(crl_file):
with open(crl_file) as fcrl:
old_checksum = hashlib.md5(fcrl.read()).hexdigest()
checksum = hashlib.md5(crl_string).hexdigest()
if checksum != old_checksum:
update_crl = True
else:
update_crl = True
if update_crl:
fd = os.open(crl_file, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0644)
try:
os.write(fd, crl_string)
finally:
os.close(fd)
self.logger.info("New CRL file was saved in %s ..." % crl_file)
if after_script:
output = popenCommunicate([os.path.realpath(after_script)])
self.logger.info("Successfully executed script '%s' with output:\n%s" % (
after_script, output))
else:
self.logger.info("crl file don't need to be updated.")
# 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/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
from __future__ import absolute_import
import datetime
import httplib
import json
import os
import ssl
from urlparse import urlparse
from cryptography import x509
from cryptography.hazmat.backends import default_backend
import cryptography.exceptions
from . import utils
__all__ = (
'CaucaseError',
'CaucaseClient',
'HTTPSOnlyCaucaseClient',
'updateCAFile',
'updateCRLFile',
)
_cryptography_backend = default_backend()
class CaucaseError(Exception):
"""
Raised when server responds with an HTTP error status.
"""
pass
def updateCAFile(url, ca_crt_path):
"""
Bootstrap anf maintain a CA file up-to-date.
url (str)
URL to caucase, ending in eithr /cas or /cau.
ca_crt_path (str)
Path to the CA certificate file, which may not exist.
Return whether an update happened (including whether an already-known
certificate expired and was discarded).
"""
if not os.path.exists(ca_crt_path):
ca_pem = CaucaseClient(
ca_url=url,
).getCA()
with open(ca_crt_path, 'w') as ca_crt_file:
ca_crt_file.write(ca_pem)
updated = True
else:
updated = False
now = datetime.datetime.utcnow()
loaded_ca_pem_list = utils.getCertList(ca_crt_path)
ca_pem_list = [
x
for x in loaded_ca_pem_list
if utils.load_ca_certificate(x).not_valid_after > now
]
ca_pem_list.extend(
CaucaseClient(
ca_url=url,
ca_crt_pem_list=ca_pem_list,
).getNewCAList(),
)
if ca_pem_list != loaded_ca_pem_list:
data = ''.join(ca_pem_list)
with open(ca_crt_path, 'w') as ca_crt_file:
ca_crt_file.write(data)
updated = True
return updated
def updateCRLFile(url, crl_path, ca_list):
"""
Bootstrap anf maintain a CRL file up-to-date.
url (str)
URL to caucase, ending in eithr /cas or /cau.
crl_path (str)
Path to the CRL file, which may not exist.
ca_list (list of cryptography.x509.Certificate instances)
One of these CA certificates must have signed the CRL for it to be
accepted.
Return whether an update happened.
"""
if os.path.exists(crl_path):
my_crl = utils.load_crl(open(crl_path).read(), ca_list)
else:
my_crl = None
latest_crl_pem = CaucaseClient(
ca_url=url,
).getCRL()
latest_crl = utils.load_crl(latest_crl_pem, ca_list)
if latest_crl != my_crl:
with open(crl_path, 'w') as crl_file:
crl_file.write(latest_crl_pem)
return True
return False
class CaucaseClient(object):
"""
Caucase HTTP(S) client.
Expose caucase REST API as pythonic methods.
"""
def __init__(self, ca_url, ca_crt_pem_list=None, user_key=None):
# XXX: set timeout to HTTP connections ?
http_url = urlparse(ca_url)
port = http_url.port or 80
self._http_connection = httplib.HTTPConnection(
http_url.hostname,
port,
#timeout=,
)
self._ca_crt_pem_list = ca_crt_pem_list
self._path = http_url.path
if ca_crt_pem_list:
ssl_context = ssl.create_default_context(
# unicode object needed as we use PEM, otherwise create_default_context
# expects DER.
cadata=''.join(ca_crt_pem_list).decode('ascii'),
)
if user_key:
ssl_context.load_cert_chain(user_key)
self._https_connection = httplib.HTTPSConnection(
http_url.hostname,
443 if port == 80 else port + 1,
#timeout=,
context=ssl_context,
)
def _request(self, connection, method, url, body=None, headers=None):
path = self._path + url
headers = headers or {}
connection.request(method, path, body, headers)
response = connection.getresponse()
response_body = response.read()
if response.status >= 400:
raise CaucaseError(response.status, response.getheaders(), response_body)
assert response.status < 300 # caucase does not redirect
if response.status == 201:
return response.getheader('Location')
return response_body
def _http(self, method, url, body=None, headers=None):
return self._request(self._http_connection, method, url, body, headers)
def _https(self, method, url, body=None, headers=None):
return self._request(self._https_connection, method, url, body, headers)
def getCRL(self):
"""
[ANONYMOUS] Retrieve latest CRL.
"""
return self._http('GET', '/crl')
def getCSR(self, csr_id):
"""
[ANONYMOUS] Retrieve an CSR by its identifier.
"""
return self._http('GET', '/csr/%i' % (csr_id, ))
def getCSRList(self):
"""
[AUTHENTICATED] Retrieve all pending CSRs.
"""
return [
{
y.encode('ascii'): z.encode('ascii') if isinstance(z, unicode) else z
for y, z in x.iteritems()
}
for x in json.loads(self._https('GET', '/csr'))
]
def putCSR(self, csr):
"""
[ANONYMOUS] Store a CSR and return its identifier.
"""
return int(self._http('PUT', '/csr', csr, {
'Content-Type': 'application/pkcs10',
}))
def deleteCSR(self, csr_id):
"""
[AUTHENTICATED] Reject a pending CSR.
"""
self._https('DELETE', '/csr/%i' % (csr_id, ))
def _getCRT(self, crt_id):
return self._http('GET', '/crt' + crt_id)
def getCRT(self, csr_id):
"""
[ANONYMOUS] Retrieve CRT by its identifier (same as corresponding CRL
identifier).
"""
return self._getCRT('/%i' % (csr_id, ))
def getCA(self):
"""
[ANONYMOUS] Retrieve current CA certificate.
"""
return self._getCRT('/ca.crt.pem')
def getNewCAList(self):
"""
[ANONYMOUS] Retrieve CA certificate chain, with CA certificate N+1 signed
by CA certificate N, allowing automated CA cert rollout.
"""
found = False
previous_ca = trust_anchor = sorted(
(
utils.load_ca_certificate(x)
for x in self._ca_crt_pem_list
),
key=lambda x: x.not_valid_before,
)[-1]
result = []
for entry in json.loads(self._getCRT('/ca.crt.json')):
try:
payload = utils.unwrap(
entry,
lambda x: x['old_pem'],
utils.DEFAULT_DIGEST_LIST,
)
except cryptography.exceptions.InvalidSignature:
continue
if not found:
found = utils.load_ca_certificate(
payload['old_pem'].encode('ascii'),
) == trust_anchor
if found:
if utils.load_ca_certificate(
payload['old_pem'].encode('ascii'),
) != previous_ca:
raise ValueError('CA signature chain broken')
new_pem = payload['new_pem'].encode('ascii')
result.append(new_pem)
previous_ca = utils.load_ca_certificate(new_pem)
return result
def renewCRT(self, old_crt, old_key, key_len):
"""
[ANONYMOUS] Request certificate renewal.
"""
new_key = utils.generatePrivateKey(key_len=key_len)
return (
utils.dump_privatekey(new_key),
self._http(
'PUT',
'/crt/renew',
json.dumps(
utils.wrap(
{
'crt_pem': utils.dump_certificate(old_crt),
'renew_csr_pem': utils.dump_certificate_request(
x509.CertificateSigningRequestBuilder(
).subject_name(
# Note: caucase server ignores this, but cryptography
# requires CSRs to have a subject.
old_crt.subject,
).sign(
private_key=new_key,
algorithm=utils.DEFAULT_DIGEST_CLASS(),
backend=_cryptography_backend,
),
),
},
old_key,
utils.DEFAULT_DIGEST,
),
),
{'Content-Type': 'application/json'},
),
)
def revokeCRT(self, crt, key=None):
"""
Revoke certificate.
[ANONYMOUS] if key is provided.
[AUTHENTICATED] if key is missing.
"""
if key:
method = self._http
data = utils.wrap(
{
'revoke_crt_pem': crt,
},
utils.load_privatekey(key),
utils.DEFAULT_DIGEST,
)
else:
method = self._https
data = utils.nullWrap({
'revoke_crt_pem': crt,
})
method(
'PUT',
'/crt/revoke',
json.dumps(data),
{'Content-Type': 'application/json'},
)
def revokeSerial(self, serial):
"""
Revoke certificate by serial.
This method is dangerous ! Prefer revokeCRT whenever possible.
[AUTHENTICATED]
"""
self._https(
'PUT',
'/crt/revoke',
json.dumps(utils.nullWrap({'revoke_serial': serial})),
{'Content-Type': 'application/json'},
)
def signCSR(self, csr_id, template_csr=''):
"""
[AUTHENTICATED] Sign certificate signing request.
"""
header_dict = {}
if template_csr:
header_dict['Content-Type'] = 'application/pkcs10'
self._https('PUT', '/crt/%i' % (csr_id, ), template_csr, header_dict)
class HTTPSOnlyCaucaseClient(CaucaseClient):
"""
Like CaucaseClient, but forces anonymous accesses to go through HTTPS as
well.
"""
def __init__(self, *args, **kw):
super(HTTPSOnlyCaucaseClient, self).__init__(*args, **kw)
self._http_connection = self._https_connection
......@@ -15,6 +15,9 @@
#
# You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
class CertificateAuthorityException(Exception):
"""Base exception"""
......@@ -29,23 +32,9 @@ class NotFound(CertificateAuthorityException):
pass
class Found(CertificateAuthorityException):
"""Requested ID is already in use"""
class BadSignature(CertificateAuthorityException):
"""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"""
"""Resource to create already exists"""
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/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
from __future__ import absolute_import
import argparse
import datetime
import glob
import os
import signal
import socket
from SocketServer import ThreadingMixIn
import ssl
import sys
import tempfile
from threading import Thread
from urlparse import urlparse
from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from . import utils
from .wsgi import Application
from .ca import CertificateAuthority, UserCertificateAuthority
from .storage import SQLite3Storage
_cryptography_backend = default_backend()
BACKUP_SUFFIX = '.sql.caucased'
class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
"""
Threading WSGI server
"""
daemon_threads = True
def __init__(self, server_address, *args, **kw):
self.address_family, _, _, _, _ = socket.getaddrinfo(*server_address)[0]
assert self.address_family in (socket.AF_INET, socket.AF_INET6), self.address_family
WSGIServer.__init__(self, server_address, *args, **kw)
class CaucaseWSGIRequestHandler(WSGIRequestHandler):
"""
Make WSGIRequestHandler logging more apache-like.
"""
def log_date_time_string(self):
"""
Apache-style date format.
Compared to python's default (from BaseHTTPServer):
- ":" between day and time
- "+NNNN" timezone is displayed
- ...but, because of how impractical it is in python to get system current
timezone (including DST considerations), time it always logged in GMT
"""
now = datetime.datetime.utcnow()
return now.strftime('%d/' + self.monthname[now.month] + '/%Y:%H:%M:%S +0000')
class CaucaseSSLWSGIRequestHandler(CaucaseWSGIRequestHandler):
"""
Add SSL_CLIENT_CERT to environ when client has sent a certificate.
"""
ssl_client_cert_serial = '-'
def get_environ(self):
environ = WSGIRequestHandler.get_environ(self)
client_cert_der = self.request.getpeercert(binary_form=True)
if client_cert_der is not None:
cert = x509.load_der_x509_certificate(
client_cert_der,
_cryptography_backend,
)
self.ssl_client_cert_serial = str(cert.serial_number)
environ['SSL_CLIENT_CERT'] = utils.dump_certificate(cert)
return environ
# pylint: disable=redefined-builtin
def log_message(self, format, *args):
# Note: compared to BaseHTTPHandler, logs the client certificate serial as
# user name.
sys.stderr.write(
"%s - %s [%s] %s\n" % (
self.client_address[0],
self.ssl_client_cert_serial,
self.log_date_time_string(),
format % args,
)
)
# pylint: enable=redefined-builtin
def startServerThread(server):
"""
Create and start a "serve_forever" thread, and return it.
"""
server_thread = Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
def updateSSLContext(
https,
key_len,
threshold,
server_key_path,
hostname,
cau,
cas,
wrap=False,
):
"""
Build a new SSLContext with updated CA certificates, CRL and server key pair,
apply it to <https>.socket and return the datetime of next update.
"""
ssl_context = ssl.create_default_context(
purpose=ssl.Purpose.CLIENT_AUTH,
)
# SSL is used for client authentication, and is only required for very few
# users on any given caucased. So make ssl_context even stricter than python
# does.
# No TLSv1 and TLSv1.1, leaving (currently) only TLSv1.2
ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
# If a client wishes to use https for unauthenticated operations, that's
# fine too.
ssl_context.verify_mode = ssl.CERT_OPTIONAL
# Note: it does not seem possible to get python's openssl context to check
# certificate revocation:
# - calling load_verify_locations(cadata=<crl data>) or
# load_verify_locations(cadata=<crl data> + <ca crt data>) raises
# - calling load_verify_locations(cadata=<ca crt data> + <crl data>) fails to
# validate CA completely
# Anyway, wsgi application level is supposed (and automatically tested to)
# verify revocations too, so this should not be a big issue... Still,
# implementation cross-check would have been nice.
#ssl_context.verify_flags = ssl.VERIFY_CRL_CHECK_LEAF
ssl_context.load_verify_locations(
cadata=cau.getCACertificate().decode('ascii'),
)
cas_certificate_list = cas.getCACertificateList()
threshold_delta = datetime.timedelta(threshold, 0)
if os.path.exists(server_key_path):
old_crt_pem = utils.getCert(server_key_path)
old_crt = utils.load_certificate(old_crt_pem, cas_certificate_list, None)
if old_crt.not_valid_after - threshold_delta < datetime.datetime.utcnow():
new_key = utils.generatePrivateKey(key_len)
new_key_pem = utils.dump_privatekey(new_key)
new_crt_pem = cas.renew(
crt_pem=old_crt_pem,
csr_pem=utils.dump_certificate_request(
x509.CertificateSigningRequestBuilder(
).subject_name(
# Note: caucase server ignores this, but cryptography
# requires CSRs to have a subject.
old_crt.subject,
).sign(
private_key=new_key,
algorithm=utils.DEFAULT_DIGEST_CLASS(),
backend=_cryptography_backend,
),
),
)
with open(server_key_path, 'w') as crt_file:
crt_file.write(new_key_pem)
crt_file.write(new_crt_pem)
else:
new_key = utils.generatePrivateKey(key_len)
csr_id = cas.appendCertificateSigningRequest(
csr_pem=utils.dump_certificate_request(
x509.CertificateSigningRequestBuilder(
).subject_name(
x509.Name([
x509.NameAttribute(
oid=x509.oid.NameOID.COMMON_NAME,
value=hostname.decode('ascii'),
),
]),
).add_extension(
x509.KeyUsage(
# pylint: disable=bad-whitespace
digital_signature =True,
content_commitment=False,
key_encipherment =True,
data_encipherment =False,
key_agreement =False,
key_cert_sign =False,
crl_sign =False,
encipher_only =False,
decipher_only =False,
# pylint: enable=bad-whitespace
),
critical=True,
).sign(
private_key=new_key,
algorithm=utils.DEFAULT_DIGEST_CLASS(),
backend=_cryptography_backend,
),
),
override_limits=True,
)
cas.createCertificate(csr_id)
new_crt_pem = cas.getCertificate(csr_id)
new_key_pem = utils.dump_privatekey(new_key)
old_mask = os.umask(077)
try:
with open(server_key_path, 'w') as crt_file:
crt_file.write(new_key_pem)
crt_file.write(new_crt_pem)
finally:
os.umask(old_mask)
ssl_context.load_cert_chain(server_key_path)
if wrap:
https.socket = ssl_context.wrap_socket(
sock=https.socket,
server_side=True,
)
else:
https.socket.context = ssl_context
return utils.load_certificate(
utils.getCert(server_key_path),
cas_certificate_list,
None,
).not_valid_after - threshold_delta
def main(argv=None):
"""
Caucase stand-alone http server.
"""
parser = argparse.ArgumentParser(description='caucased')
parser.add_argument(
'--db',
default='caucase.sqlite',
help='Path to the SQLite database. default: %(default)s',
)
parser.add_argument(
'--server-key',
default='server.key.pem',
metavar='KEY_PATH',
help='Path to the ssl key pair to use for https socket. '
'default: %(default)s',
)
parser.add_argument(
'--netloc',
required=True,
help='<host>[:<port>] of HTTP socket. '
'HTTPS socket netloc will be deduced following caucase rules: if port is '
'80 or not provided, https port will be 443, else it will be port + 1. '
'If not provided, http port will be picked among available ports and '
'https port will be the next port. Also, signed certificates will not '
'contain a CRL distribution point URL. If https port is not available, '
'this program will exit with an aerror status. '
'Note on encoding: only ascii is currently supported. Non-ascii may be '
'provided idna-encoded.',
)
parser.add_argument(
'--threshold',
default=31,
type=float,
help='The remaining certificate validity period, in days, under '
'which a renew is desired. default: %(default)s',
)
parser.add_argument(
'--key-len',
default=2048,
type=int,
metavar='BITLENGTH',
help='Number of bits to use when generating a new private key. '
'default: %(default)s',
)
service_group = parser.add_argument_group(
'CAS options: normal certificates, which are not given any privilege on '
'caucased',
)
user_group = parser.add_argument_group(
'CAU options: special certificates, which are allowed to sign other '
'certificates and can decrypt backups',
)
service_group.add_argument(
'--service-crt-validity',
default=3 * 31,
type=float,
metavar='DAYS',
help='Number of days an issued certificate is valid for. '
'default: %(default)s',
)
user_group.add_argument(
'--user-crt-validity',
default=3 * 31,
type=float,
metavar='DAYS',
help='Number of days an issued certificate is valid for. '
'default: %(default)s',
)
service_group.add_argument(
'--service-max-csr',
default=50,
type=int,
help='Maximum number of pending CSR. Further CSR get refused until '
'an existing ones gets signed or rejected. default: %(default)s',
)
user_group.add_argument(
'--user-max-csr',
default=50,
type=int,
help='Maximum number of pending CSR. Further CSR get refused until '
'an existing ones gets signed or rejected. default: %(default)s',
)
service_group.add_argument(
'--service-auto-approve-count',
default=0,
type=int,
metavar='COUNT',
help='Number service certificates which should be automatically signed on '
'submission, excluding the one needed to serve caucase. '
'default: %(default)s'
)
user_group.add_argument(
'--user-auto-approve-count',
default=1,
type=int,
metavar='COUNT',
help='Number of user certificates which should be automatically signed on '
'submission. default: %(default)s',
)
parser.add_argument(
'--lock-auto-approve-count',
action='store_true',
help='The first time this option is given, --service-auto-approve-count '
'and --user-auto-approve-count values are stored inside caucase database '
'and will not be altered by further invocations. Once the respective '
'certificate issuance counters reach these values, no further '
'certificates will be ever automatically signed.'
)
backup_group = parser.add_argument_group(
'Backup options',
)
backup_group.add_argument(
'--backup-directory',
help='Backup file path template. Backups will be periodically stored in '
'given directory, encrypted with all certificates which are valid at the '
'time of backup generation. Any one of the associated private keys can '
'decypher it. If not set, no backup will be created.',
)
backup_group.add_argument(
'--backup-period',
default=1,
type=float,
help='Number of days between backups. default: %(default)s'
)
backup_group.add_argument(
'--restore-backup',
nargs=4,
metavar=('BACKUP_PATH', 'KEY_PATH', 'CSR_PATH', 'CRT_PATH'),
help='Restore the file at BACKUP_PATH, decyphering it with the key '
'at KEY_PATH, revoking corresponding certificate and issuing a new '
'one in CRT_PATH using the public key in CSR_PATH. '
'If database is not empty, nothing is done. '
'Then process will exit and must be restarted wihtout this option.',
)
args = parser.parse_args(argv)
# pylint: disable=unused-argument
def onTERM(signum, stack):
"""
Sigterm handler
"""
# The main objective of this signal hander is to fix coverage scores:
# without it, it seems hits generated by this process do not get
# accounted for (atexit not called ?). With it, interpreter shutdown
# seems nicer.
raise SystemExit
# pylint: enable=unused-argument
signal.signal(signal.SIGTERM, onTERM)
base_url = u'http://' + args.netloc.decode('ascii')
parsed_base_url = urlparse(base_url)
hostname = parsed_base_url.hostname
http_port = parsed_base_url.port
cau_crt_life_time = args.user_crt_validity
cau_db_kw = {
'table_prefix': 'cau',
'max_csr_amount': args.user_max_csr,
# Effectively disables certificate expiration
'crt_keep_time': cau_crt_life_time,
'crt_read_keep_time': cau_crt_life_time,
'enforce_unique_key_id': True,
}
cau_kw = {
'ca_subject_dict': {
'CN': u'Caucase CAU' + (
u'' if base_url is None else u' at ' + base_url + '/cau'
),
},
'ca_key_size': args.key_len,
'crt_life_time': cau_crt_life_time,
'auto_sign_csr_amount': args.user_auto_approve_count,
'lock_auto_sign_csr_amount': args.lock_auto_approve_count,
}
if args.restore_backup:
(
backup_path,
backup_key_path,
backup_csr_path,
backup_crt_path,
) = args.restore_backup
try:
_, key_pem, _ = utils.getKeyPair(backup_key_path)
except ValueError:
# maybe user extracted their private key ?
key_pem = utils.getKey(backup_key_path)
with open(backup_path) as backup_file:
with open(backup_crt_path, 'a') as new_crt_file:
new_crt_file.write(
UserCertificateAuthority.restoreBackup(
db_class=SQLite3Storage,
db_path=args.db,
read=backup_file.read,
key_pem=key_pem,
csr_pem=utils.getCertRequest(backup_csr_path),
db_kw=cau_db_kw,
kw=cau_kw,
),
)
return
cau = UserCertificateAuthority(
storage=SQLite3Storage(
db_path=args.db,
**cau_db_kw
),
**cau_kw
)
cas = CertificateAuthority(
storage=SQLite3Storage(
db_path=args.db,
table_prefix='cas',
max_csr_amount=args.service_max_csr,
),
ca_subject_dict={
'CN': u'Caucase CAS' + (
u'' if base_url is None else u' at ' + base_url + '/cas'
),
},
crl_base_url=None if base_url is None else base_url + u'/cas/crl',
ca_key_size=args.key_len,
crt_life_time=args.service_crt_validity,
auto_sign_csr_amount=args.service_auto_approve_count,
lock_auto_sign_csr_amount=args.lock_auto_approve_count,
)
application = Application(cau=cau, cas=cas)
http = make_server(
host=hostname,
port=http_port,
app=application,
server_class=ThreadingWSGIServer,
handler_class=CaucaseWSGIRequestHandler,
)
https = make_server(
host=hostname,
port=443 if http_port == 80 else http_port + 1,
app=application,
server_class=ThreadingWSGIServer,
handler_class=CaucaseSSLWSGIRequestHandler,
)
next_deadline = next_ssl_update = updateSSLContext(
https=https,
key_len=args.key_len,
threshold=args.threshold,
server_key_path=args.server_key,
hostname=hostname,
cau=cau,
cas=cas,
wrap=True,
)
if args.backup_directory:
backup_period = datetime.timedelta(args.backup_period, 0)
try:
next_backup = max(
datetime.datetime.utcfromtimestamp(os.stat(x).st_ctime)
for x in glob.iglob(
os.path.join(args.backup_directory, '*' + BACKUP_SUFFIX),
)
) + backup_period
except ValueError:
next_backup = datetime.datetime.utcnow()
next_deadline = min(
next_deadline,
next_backup,
)
else:
next_backup = None
startServerThread(http)
startServerThread(https)
try:
while True:
now = utils.until(next_deadline)
if now >= next_ssl_update:
next_ssl_update = updateSSLContext(
https=https,
key_len=args.key_len,
threshold=args.threshold,
server_key_path=args.server_key,
hostname=hostname,
cau=cau,
cas=cas,
)
if next_backup is None:
next_deadline = next_ssl_update
else:
if now >= next_backup:
tmp_backup_fd, tmp_backup_path = tempfile.mkstemp(
prefix='caucase_backup_',
)
with os.fdopen(tmp_backup_fd, 'w') as backup_file:
result = cau.doBackup(backup_file.write)
if result:
backup_path = os.path.join(
args.backup_directory,
now.strftime('%Y%m%d%H%M%S') + BACKUP_SUFFIX,
)
os.rename(tmp_backup_path, backup_path)
next_backup = now + backup_period
else:
os.unlink(tmp_backup_path)
next_backup = now + datetime.timedelta(0, 3600)
next_deadline = min(
next_ssl_update,
next_backup,
)
except utils.SleepInterrupt:
pass
.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
......@@ -15,591 +15,546 @@
#
# You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
from __future__ import absolute_import
from random import getrandbits
import os
import errno
import uuid
import hashlib
from datetime import datetime, timedelta
from OpenSSL import crypto
from caucase import db
from caucase import utils
from caucase.exceptions import (NoStorage, NotFound, Found, ExpiredCertificate)
from flask_user import UserMixin
STATUS_VALIDATED = 'validated'
STATUS_REVOKED = 'invalidated'
STATUS_REJECTED = 'rejected'
STATUS_PENDING = 'pending'
class Storage(object):
def __init__(self, db_instance, max_csr_amount=None,
crt_keep_time=None, csr_keep_time=None):
self.db = db_instance
# initialise tables
self.db.create_all()
# store some config in storage
if max_csr_amount:
self.__setConfig('max-csr-amount', max_csr_amount)
if crt_keep_time is not None:
self.__setConfig('crt-keep-time', crt_keep_time) # 0 mean always keep in storage
if csr_keep_time is not None:
self.__setConfig('csr-keep-time', csr_keep_time) # 0 mean always keep non pending csr in storage
def _getConfig(self, key):
return Config.query.filter(Config.key == key).first()
def __setConfig(self, key, value):
"""
Add new config to storage
"""
entry = self._getConfig(key)
if not entry:
entry = Config(key=key, value='%s' % value)
self.db.session.add(entry)
import sqlite3
from threading import local
from time import time
from .exceptions import NoStorage, NotFound, Found
__all__ = ('SQLite3Storage', )
DAY_IN_SECONDS = 60 * 60 * 24
class SQLite3Storage(local):
"""
CA data storage.
Every cryptographic type this class deals with must be PEM-encoded.
"""
def __init__(
self,
db_path,
table_prefix,
max_csr_amount=50,
crt_keep_time=1,
crt_read_keep_time=0.05, # About 1 hour
enforce_unique_key_id=False,
):
"""
db_path (str)
SQLite connection string.
table_prefix (str)
Name to use as a prefix for all tables managed by this storage adapter.
Allows sharing the database, although it should only be within the same
caucase process for access permission reasons.
max_csr_amount (int)
Maximum number of allowed pending certificate signing requests.
To prevent flood.
crt_keep_time (float)
Time to keep signed certificates for, in days.
crt_read_keep_time (float)
Time to keep CRT content for after it was first read, in days.
Allows requester to fail retrieving the certificate by tolerating
retries.
enforce_unique_key_id (bool)
When true, certificate requests cannot be appended if there is already
and known entry for the same private key.
Note: this only makes sense if crt_keep_time and crt_read_keep_time are
set at least to the certificate life span.
Useful for backups, to ensure the certificate to revoke can be uniquely
identified from the key used to decrypt the backup archive.
"""
super(SQLite3Storage, self).__init__()
self._db = db = sqlite3.connect(db_path)
self._table_prefix = table_prefix
db.row_factory = sqlite3.Row
self._max_csr_amount = max_csr_amount
self._crt_keep_time = crt_keep_time * DAY_IN_SECONDS
self._crt_read_keep_time = crt_read_keep_time * DAY_IN_SECONDS
with db:
# Note about revoked.serial: certificate serials exceed the 63 bits
# sqlite can accept as integers, so store these as text. Use a trivial
# string serialisation: not very space efficient, but this should not be
# a limiting issue for our use-cases anyway.
db.cursor().executescript('''
CREATE TABLE IF NOT EXISTS %(prefix)sca (
expiration_date INTEGER,
key TEXT,
crt TEXT
);
CREATE TABLE IF NOT EXISTS %(prefix)scrt (
id INTEGER PRIMARY KEY,
key_id TEXT %(key_id_constraint)s,
expiration_date INTEGER,
csr TEXT,
crt TEXT
);
CREATE TABLE IF NOT EXISTS %(prefix)srevoked (
serial TEXT PRIMARY KEY,
revocation_date INTEGER,
expiration_date INTEGER
);
CREATE TABLE IF NOT EXISTS %(prefix)scrl (
expiration_date INTEGER,
crl TEXT
);
CREATE TABLE IF NOT EXISTS %(prefix)scounter (
name TEXT PRIMARY KEY,
value INTEGER
);
CREATE TABLE IF NOT EXISTS %(prefix)sconfig_once (
name TEXT PRIMARY KEY,
value TEXT
)
''' % {
'prefix': table_prefix,
'key_id_constraint': 'UNIQUE' if enforce_unique_key_id else '',
})
def _incrementCounter(self, name, increment=1, initial=0):
"""
Increment counter with <name> by <increment> and return resulting value.
If <name> is not found, it is created with <initial>, and then incremented.
Does not commit.
"""
row = self._executeSingleRow(
'SELECT value FROM %scounter WHERE name = ? LIMIT 2' % (
self._table_prefix,
),
(name, ),
)
if row is None:
value = initial
else:
# update value
entry.value = value
self.db.session.commit()
value = row['value']
value += increment
self._db.cursor().execute(
'INSERT OR REPLACE INTO %scounter (name, value) VALUES (?, ?)' % (
self._table_prefix,
),
(name, value),
)
return value
def getConfig(self, key, default=None):
def _executeSingleRow(self, sql, parameters=()):
"""
Return a config value or default
Execute <sql>, raise if it produces more than 1 row, and return it.
"""
result_list = self._db.cursor().execute(sql, parameters).fetchall()
if result_list:
result, = result_list
return result
def getConfigOnce(self, name, default):
"""
entry = self._getConfig(key)
if not entry:
Retrieve the value of <name> from config-once list, or <default> if not
stored.
"""
with self._db:
result = self._executeSingleRow(
'SELECT value FROM %sconfig_once WHERE name = ?' % (
self._table_prefix,
),
(name, ),
)
if result is None:
return default
return entry.value
return result['value']
def _getMaxCsrCount(self):
return int(self.getConfig('max-csr-amount', 50))
def setConfigOnce(self, name, value):
"""
Store <value> as <name> in config-once list, if it was not already stored.
If it was already stored, do nothing.
"""
try:
with self._db as db:
db.cursor().execute(
'INSERT INTO %sconfig_once (name, value) VALUES (?, ?)' % (
self._table_prefix,
),
(name, value),
)
except sqlite3.IntegrityError:
pass
def getCAKeyPairList(self):
"""
Return the chronologically sorted (oldest in [0], newest in [-1]) certificate authority
key pairs.
Return the chronologically sorted (oldest in [0], newest in [-1])
certificate authority key pairs.
"""
item_list = CAKeypair.query.filter(
CAKeypair.active == True
).order_by(
CAKeypair.creation_date.asc()
).all()
if not item_list:
return []
keypair_list = []
for keypair in item_list:
keypair_list.append({
'crt': crypto.load_certificate(crypto.FILETYPE_PEM, keypair.certificate),
'key': crypto.load_privatekey(crypto.FILETYPE_PEM, keypair.key)
})
return keypair_list
with self._db as db:
c = db.cursor()
c.execute(
'DELETE FROM %sca WHERE expiration_date < ?' % (
self._table_prefix,
),
(time(), ),
)
return [
{
'crt_pem': x['crt'].encode('ascii'),
'key_pem': x['key'].encode('ascii'),
}
for x in db.cursor().execute(
'SELECT key, crt FROM %sca ORDER BY expiration_date ASC' % (
self._table_prefix,
),
).fetchall()
]
def storeCAKeyPair(self, key_pair):
def appendCAKeyPair(self, expiration_timestamp, key_pair):
"""
Store a certificate authority key pair.
"""
serial = utils.getSerialToInt(key_pair['crt'])
crt_string = crypto.dump_certificate(crypto.FILETYPE_PEM, key_pair['crt'])
key_string = crypto.dump_privatekey(crypto.FILETYPE_PEM, key_pair['key'])
# check that keypair is not stored
keypair = CAKeypair.query.filter(
CAKeypair.active == True
).filter(
CAKeypair.serial == serial
).first()
if keypair:
raise Found('Another CA certificate exists with serial %r' % (serial, ))
saved_pair = CAKeypair(
serial=serial,
common_name=key_pair['crt'].get_subject().CN,
expire_after=datetime.strptime(
key_pair['crt'].get_notAfter(), '%Y%m%d%H%M%SZ'
expiration_timestamp (int)
Unix GMT timestamp of CA certificate "valid until" date.
key_pair (dict with 'key' and 'crt' items)
CA key pair to store, as bytes.
"""
with self._db as db:
db.cursor().execute(
'INSERT INTO %sca (expiration_date, key, crt) VALUES (?, ?, ?)' % (
self._table_prefix,
),
start_before=datetime.strptime(
key_pair['crt'].get_notBefore(), '%Y%m%d%H%M%SZ'
(
expiration_timestamp,
key_pair['key_pem'],
key_pair['crt_pem'],
),
key=key_string,
certificate=crt_string,
active=True,
creation_date=datetime.utcnow()
)
self.db.session.add(saved_pair)
self.db.session.commit()
def storeCertificateSigningRequest(self, csr):
def appendCertificateSigningRequest(self, csr_pem, key_id, override_limits=False):
"""
Store acertificate signing request and generate a unique ID for it.
Note: ID uniqueness is only guaranteed among pending CSR, and may be reused
after the original CSR has been discarded (by being rejected or signed).
"""
csr_amount = self.countPendingCertificateSiningRequest()
if csr_amount >= self._getMaxCsrCount():
raise NoStorage('Too many pending CSRs')
content = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
checksum = hashlib.md5(content).hexdigest()
check_csr = CertificateRequest.query.filter(
CertificateRequest.status == STATUS_PENDING
).filter(
CertificateRequest.checksum == checksum
).first()
if check_csr:
# this only prevent client loop sending the same csr until csr_amount is reached
return check_csr.csr_id
key = str(uuid.uuid1().hex)
csr_id = '%s.csr.pem' % key
crt_id = '%s.crt.pem' % key
req = CertificateRequest(
content=content,
creation_date=datetime.utcnow(),
common_name=csr.get_subject().CN,
checksum=checksum,
csr_id=csr_id,
crt_id=crt_id)
request_amount = self._getConfig('csr-requested-amount')
if not request_amount:
request_amount = Config(key='csr-requested-amount', value='1')
self.db.session.add(request_amount)
else:
request_amount.value = '%s' % (int(request_amount.value) + 1,)
self.db.session.add(req)
self.db.session.commit()
return csr_id
def deletePendingCertificateRequest(self, csr_id):
csr = CertificateRequest.query.filter(
CertificateRequest.status == STATUS_PENDING
).filter(
CertificateRequest.csr_id == csr_id
).first()
if csr:
self.db.session.delete(csr)
self.db.session.commit()
with self._db as db:
known_csr = self._executeSingleRow(
'SELECT id FROM %scrt WHERE csr = ? LIMIT 2' % (
self._table_prefix,
),
(csr_pem, ),
)
if known_csr is not None:
return known_csr['id'], None
if override_limits:
# Ignore max pending count
# Do not increment the number of auto-signed certificates, but do not
# automatically sign either.
requested_count = None
else:
raise NotFound('No pending CSR with id %r' % (csr_id, ))
def getPendingCertificateRequest(self, csr_id):
csr = CertificateRequest.query.filter(
CertificateRequest.status == STATUS_PENDING
).filter(
CertificateRequest.csr_id == csr_id
).first()
if csr:
return csr.content
raise NotFound('No pending CSR with id %r' % (csr_id, ))
def getPendingCertificateRequestList(self, limit=0, with_data=False):
"""
Return list of all CSR
"""
data_list = []
index = 1
query = CertificateRequest.query.filter(
CertificateRequest.status == STATUS_PENDING
if self._executeSingleRow(
'SELECT COUNT(*) FROM %scrt WHERE crt IS NULL' % (
self._table_prefix,
)
if limit > 0:
query.limit(limit)
csr_list = query.all()
for request_csr in csr_list:
csr = {
'index': index,
'csr_id': request_csr.csr_id,
'crt_id': request_csr.crt_id,
'common_name': request_csr.common_name,
'creation_date': request_csr.creation_date
}
if with_data:
certificate['content'] = request_csr.content
data_list.append(csr)
index += 1
return data_list
def storeCertificate(self, csr_id, crt):
"""
Store certificate as crt_id. crt is a certificate PEM object.
"""
csr = csr = CertificateRequest.query.filter(
CertificateRequest.status == STATUS_PENDING
).filter(
CertificateRequest.csr_id == csr_id
).first()
if not csr:
raise NotFound('No pending CSR with id %r' % (csr_id, ))
cert = Certificate.query.filter(
Certificate.status == STATUS_VALIDATED
).filter(
Certificate.crt_id == csr.crt_id
).first()
if cert:
raise Found('CRT already exists')
cert_db = Certificate(
crt_id=csr.crt_id,
serial=utils.getSerialToInt(crt),
common_name=crt.get_subject().CN,
expire_after=datetime.strptime(crt.get_notAfter(), '%Y%m%d%H%M%SZ'),
start_before=datetime.strptime(crt.get_notBefore(), '%Y%m%d%H%M%SZ'),
creation_date=datetime.utcnow(),
content=crypto.dump_certificate(crypto.FILETYPE_PEM, crt)
)[0] >= self._max_csr_amount:
raise NoStorage
requested_count = self._incrementCounter('received_csr')
csr_id = getrandbits(63)
c = db.cursor()
c.execute(
'INSERT INTO %scrt (id, key_id, csr) VALUES (?, ?, ?)' % (
self._table_prefix,
),
(
csr_id,
key_id,
csr_pem,
),
)
# Change Csr status as 'validated', so it can be trashed
csr.status = STATUS_VALIDATED
self.db.session.add(cert_db)
self.db.session.commit()
return csr.crt_id
def getCertificateFromSerial(self, serial):
cert = Certificate.query.filter(
Certificate.status == STATUS_VALIDATED
).filter(
Certificate.serial == '%s' % serial
).first()
if cert:
return cert
raise NotFound('No certficate with serial %r' % (serial, ))
# schedule certificate removal
def getCertificate(self, crt_id):
cert = Certificate.query.filter(
Certificate.status == STATUS_VALIDATED
).filter(
Certificate.crt_id == crt_id
).first()
if cert and cert.content:
# if content is emtpy, maybe the certificate content was stripped ?
return cert.content
raise NotFound('No certficate with id %r' % (crt_id, ))
# schedule certificate removal
def getSignedCertificateList(self, limit=0, with_data=False):
data_list = []
index = 1
query = Certificate.query.filter(
Certificate.status == STATUS_VALIDATED
c.execute(
'DELETE FROM %scrt WHERE expiration_date < ?' % (
self._table_prefix,
),
(time(), ),
)
if limit > 0:
query.limit(limit)
signed_cert_list = query.all()
for signed_cert in signed_cert_list:
certificate = {
'index': index,
'serial': signed_cert.serial,
'crt_id': signed_cert.crt_id,
'common_name': signed_cert.common_name,
'expire_after': signed_cert.expire_after,
'start_before': signed_cert.start_before,
}
if with_data:
certificate['content'] = signed_cert.content
data_list.append(certificate)
index += 1
return data_list
return csr_id, requested_count
def revokeCertificate(self, serial=None, crt_id=None, reason=''):
def deletePendingCertificateSigningRequest(self, csr_id):
"""
Add serial to the list of revoked certificates.
Associated certificate must expire at (or before) not_after_date, so
revocation can be pruned.
serial or crt_id should be send to get the certificate. If both are set,
serial is used.
Forget about a pending CSR. Does nothing if the CSR was already signed
(it will be automatically garbage-collected later).
Raises NotFound if there is no matching CSR.
"""
if serial is None and crt_id is None:
raise ValueError("serial or crt_id are not set to revokeCertificate.")
query = Certificate.query.filter(Certificate.status == STATUS_VALIDATED)
if serial:
query = query.filter(Certificate.serial == serial)
else:
query = query.filter(Certificate.crt_id == crt_id)
cert = query.first()
if not cert:
raise NotFound('No certficate with serial or id %r found!' % (
serial or crt_id, ))
expire_in = cert.expire_after - datetime.utcnow()
if expire_in.days < 0:
raise ExpiredCertificate("Certificate with serial %r has expired" \
" since %r day(s)." % (serial, -1*expire_in.days))
revoke = Revocation(
serial=cert.serial,
creation_date=datetime.utcnow(),
reason=reason,
crt_expire_after=cert.expire_after
with self._db as db:
c = db.cursor()
c.execute(
'DELETE FROM %scrt WHERE id = ? AND crt IS NULL' % (
self._table_prefix,
),
(csr_id, ),
)
# Set latest CRL as expired, it will be regenerated
crl = CertificateRevocationList.query.filter(
CertificateRevocationList.active == True
).first()
if crl:
crl.active = False
# this certificate is not valid anymore
cert.status = STATUS_REVOKED
self.db.session.add(revoke)
self.db.session.commit()
if c.rowcount == 1:
return
raise NotFound
def getCertificateRevocationList(self):
"""
Get Certificate Rovocation List of None if there is no valid CRL
def getCertificateSigningRequest(self, csr_id):
"""
last_revocation = CertificateRevocationList.query.order_by(
CertificateRevocationList.id.desc()
).first()
Retrieve a PEM-encoded certificate signing request.
if last_revocation and last_revocation.active:
if (last_revocation.crl_expire_after - datetime.utcnow()).days >= 0:
return last_revocation.content
return None
def getNextCRLVersionNumber(self):
last_revocation = CertificateRevocationList.query.order_by(
CertificateRevocationList.id.desc()
).first()
if last_revocation:
return last_revocation.id + 1
else:
return 1
def storeCertificateRevocationList(self, crl, expiration_date):
"""
Store Certificate Revocation List, return stored crl string
csr_id (int)
Desired CSR id, as given when the CSR was stored.
XXX - send expiration_date because crypto.crl has no method to read this from crl object
Raises NotFound if there is no matching CSR.
"""
# Fetch cached CRL (or re-generate and store if not cached).
dumped_crl = crypto.dump_crl(crypto.FILETYPE_PEM, crl)
revocation_list = CertificateRevocationList(
creation_date=datetime.utcnow(),
crl_expire_after=expiration_date,
content=dumped_crl,
active=True
with self._db:
result = self._executeSingleRow(
'SELECT csr FROM %scrt WHERE id = ?' % (
self._table_prefix,
),
(csr_id, ),
)
self.db.session.add(revocation_list)
self.db.session.commit()
return dumped_crl
if result is None:
raise NotFound
return result['csr'].encode('ascii')
def getRevocationList(self):
"""
Get the list of all revoked certificate which are not expired
def getCertificateSigningRequestList(self):
"""
return Revocation.query.filter(
Revocation.crt_expire_after >= datetime.utcnow()
).all()
Return the list of all pending CSRs.
def getCertificateSigningRequestAmount(self):
Ignores any CSR for which a certificate was issued.
"""
Return number of CSR which was requested until now
"""
return int(self.getConfig('csr-requested-amount', 0))
def countValidatedCertificate(self):
return Certificate.query.filter(
Certificate.status == STATUS_VALIDATED
).count()
def countPendingCertificateSiningRequest(self):
return CertificateRequest.query.filter(
CertificateRequest.status == STATUS_PENDING
).count()
def countRevokedCertificate(self):
return Certificate.query.filter(
Certificate.status == STATUS_REVOKED
).count()
def countCertificateRevocation(self):
return Revocation.query.count()
with self._db as db:
return [
{
'id': x['id'],
'csr': x['csr'].encode('ascii'),
}
for x in db.cursor().execute(
'SELECT id, csr FROM %scrt WHERE crt IS NULL' % (
self._table_prefix,
),
).fetchall()
]
def storeCertificate(self, csr_id, crt):
"""
Store certificate for pre-existing CSR.
def housekeep(self):
Raises NotFound if there is no matching CSR, or if a certificate was
already stored.
"""
Remove outdated certificates (because they were retrieved long ago),
ca certificates (because they exceeded their "not valid after" date),
revocation of anway-expired certificates.
with self._db as db:
c = db.cursor()
c.execute(
'UPDATE %scrt SET crt=?, expiration_date = ? '
'WHERE id = ? AND crt IS NULL' % (
self._table_prefix,
),
(
crt,
int(time() + self._crt_keep_time),
csr_id,
),
)
if c.rowcount == 0:
raise NotFound
def getCertificate(self, crt_id):
"""
crt_keep_time = int(self.getConfig('crt-keep-time', 0))
csr_keep_time = int(self.getConfig('csr-keep-time', 0))
Retrieve a PEM-encoded certificate.
expired_keypair_list = CAKeypair.query.filter(
CAKeypair.expire_after < datetime.utcnow()
).all()
for key_pair in expired_keypair_list:
# Desactivate this ca certificate
key_pair.active = False
crt_id (int)
Desired certificate id, which is the same as corresponding CSR's id.
# wipe certificate content
if crt_keep_time > 0:
check_date = datetime.utcnow() - timedelta(0, crt_keep_time)
cert_list = Certificate.query.filter(
Certificate.creation_date <= check_date
Raises NotFound if there is no matching CRT or if no certificate was issued
for it.
"""
with self._db as db:
row = self._executeSingleRow(
'SELECT crt, expiration_date FROM %scrt '
'WHERE id = ? AND crt IS NOT NULL' % (
self._table_prefix,
),
(crt_id, ),
)
for cert in cert_list:
# clear x509 certificate information
cert.content = ""
# delete certificate request
if csr_keep_time > 0:
check_date = datetime.utcnow() - timedelta(0, csr_keep_time)
csr_list = CertificateRequest.query.filter(
CertificateRequest.status != STATUS_PENDING
).filter(
CertificateRequest.creation_date <= check_date
if row is None:
raise NotFound
new_expiration_date = int(time() + self._crt_read_keep_time)
if row['expiration_date'] > new_expiration_date:
db.cursor().execute(
'UPDATE %scrt SET expiration_date = ? WHERE id = ?' % (
self._table_prefix,
),
(
new_expiration_date,
crt_id,
)
for csr in csr_list:
self.db.session.delete(csr)
# delete all expired Certificate Rovocation
revocation_list = Revocation.query.filter(
Revocation.crt_expire_after < datetime.utcnow()
).all()
for revocation in revocation_list:
self.db.session.delete(revocation)
# Delete all expired Certificate Rovocation List (CRL)
crl_list = CertificateRevocationList.query.filter(
CertificateRevocationList.crl_expire_after < datetime.utcnow()
).all()
for crl in crl_list:
self.db.session.delete(crl)
self.db.session.commit()
def findOrCreateUser(self, first_name, last_name, email, username, hash_password):
""" Find existing user or create new user """
user = User.query.filter(User.username == username).first()
if not user:
user = User(email=email,
first_name=first_name,
last_name=last_name,
username=username,
password=hash_password,
active=True,
confirmed_at=datetime.utcnow()
)
db.session.add(user)
db.session.commit()
return user
return row['crt'].encode('ascii')
def findUser(self, username):
return User.query.filter(User.username == username).first()
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
# User authentication information
username = db.Column(db.String(50), nullable=False, unique=True)
password = db.Column(db.String(255), nullable=False, server_default='')
# User email information
email = db.Column(db.String(255), nullable=False, unique=True)
confirmed_at = db.Column(db.DateTime())
def getCertificateByKeyIdentifier(self, key_id):
"""
Return the certificate corresponding to given key_id.
# User information
active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0')
first_name = db.Column(db.String(100), nullable=False, server_default='')
last_name = db.Column(db.String(100), nullable=False, server_default='')
Raises NotFound if there is no matching CRT or if no certificate was issued
for it.
"""
with self._db:
row = self._executeSingleRow(
'SELECT crt FROM %scrt WHERE key_id = ? AND crt IS NOT NULL' % (
self._table_prefix,
),
(key_id, ),
)
if row is None:
raise NotFound
return row['crt'].encode('ascii')
def iterCertificates(self):
"""
Iterator over stored certificates.
"""
with self._db as db:
c = db.cursor()
c.execute('SELECT crt FROM %scrt WHERE crt IS NOT NULL' % (
self._table_prefix,
))
while True:
row = c.fetchone()
if row is None:
break
yield row['crt'].encode('ascii')
def revoke(self, serial, expiration_date):
"""
Add given certificate serial to the list of revoked certificates.
Flushes any current CRL.
serial (int)
Serial of the certificate to revoke.
expiration_date (int)
Unix timestamp at which the certificate expires, allowing to remove this
entry from the CRL.
"""
with self._db as db:
c = db.cursor()
c.execute('DELETE FROM %scrl' % (
self._table_prefix,
))
try:
c.execute(
'INSERT INTO %srevoked '
'(serial, revocation_date, expiration_date) '
'VALUES (?, ?, ?)' % (
self._table_prefix,
),
(
str(serial),
int(time()),
expiration_date,
)
)
except sqlite3.IntegrityError:
raise Found
class Config(db.Model):
def getCertificateRevocationList(self):
"""
This table store some configs and information
Get PEM-encoded current Certificate Revocation List.
Returns None if there is no CRL.
"""
__tablename__ = 'config'
key = db.Column(db.String(50), primary_key=True)
value = db.Column(db.Text)
with self._db:
row = self._executeSingleRow(
'SELECT crl FROM %scrl '
'WHERE expiration_date > ? ORDER BY expiration_date DESC LIMIT 1' % (
self._table_prefix,
),
(time(), )
)
if row is not None:
return row['crl'].encode('ascii')
class CAKeypair(db.Model):
def getNextCertificateRevocationListNumber(self):
"""
This table is used ca certificate key pair
Get next CRL sequence number.
"""
__tablename__ = 'ca_keypair'
id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String(50), unique=True)
expire_after = db.Column(db.DateTime)
start_before = db.Column(db.DateTime)
common_name = db.Column(db.String(50), unique=False)
active = db.Column(db.Boolean(), nullable=False, server_default='1')
certificate = db.Column(db.Text)
key = db.Column(db.Text)
creation_date = db.Column(db.DateTime)
return self._incrementCounter('crl_number')
class CertificateRequest(db.Model):
def storeCertificateRevocationList(self, crl, expiration_date):
"""
This table is used to store certificate signature request
Store Certificate Revocation List.
"""
__tablename__ = 'csr'
id = db.Column(db.Integer, primary_key=True)
csr_id=db.Column(db.String(80), unique=True)
crt_id = db.Column(db.String(80), unique=True)
common_name = db.Column(db.String(50), unique=False)
content = db.Column(db.Text)
creation_date = db.Column(db.DateTime)
status = db.Column(db.String(20), unique=False, server_default=STATUS_PENDING)
# checksum prevent to store twice the same csr
checksum = db.Column(db.String(50))
with self._db as db:
c = db.cursor()
c.execute('DELETE FROM %scrl' % (
self._table_prefix,
))
c.execute(
'INSERT INTO %scrl (expiration_date, crl) VALUES (?, ?)' % (
self._table_prefix,
),
(
int(expiration_date),
crl,
),
)
class Certificate(db.Model):
def getRevocationList(self):
"""
This table is used to store some informations about certificate
Get the list of all revoked certificates.
Returns a list of dicts, each containing:
- revocation_date (int)
Unix timestamp of certificate revocation.
- serial (int)
Revoked certificate's serial.
"""
__tablename__ = 'certificate'
id = db.Column(db.Integer, primary_key=True)
crt_id = db.Column(db.String(80), unique=True)
serial = db.Column(db.String(50), unique=True)
common_name = db.Column(db.String(50), unique=False)
expire_after = db.Column(db.DateTime)
start_before = db.Column(db.DateTime)
creation_date = db.Column(db.DateTime)
# status = validated or revoked
status = db.Column(db.String(20), unique=False, server_default=STATUS_VALIDATED)
content = db.Column(db.Text)
with self._db as db:
c = db.cursor()
c.execute(
'DELETE FROM %srevoked WHERE expiration_date < ?' % (
self._table_prefix,
),
(time(), ),
)
return [
{
'revocation_date': int(x['revocation_date']),
'serial': int(x['serial']),
}
for x in c.execute(
'SELECT revocation_date, serial FROM %srevoked' % (
self._table_prefix,
),
)
]
class Revocation(db.Model):
def dumpIterator(self):
"""
This table store certificate revocation request from users
Backs the *entire* dabase up. This is not limited to tables managed by this
class (so not limited to table_prefix).
"""
__tablename__ = 'revoked'
id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String(50), unique=False)
crt_expire_after = db.Column(db.DateTime)
reason = db.Column(db.String(200), unique=False)
creation_date = db.Column(db.DateTime)
for statement in self._db.iterdump():
yield statement.encode('utf-8') + '\0'
class CertificateRevocationList(db.Model):
@staticmethod
def restore(db_path, restorator):
"""
This table store certificate revocation list content
Restores a dump which is the concatenated output of dumpIterator.
Should not be called directly, but via
ca.CertificateAuthority.restoreBackup .
db_path (str)
Path to the SQLite database to produce. Must not exist.
read (callabla taking an integer as argument)
Must return this many bytes, or an empty string if there is no input
left.
"""
__tablename__ = 'crl'
id = db.Column(db.Integer, primary_key=True)
active = db.Column(db.Boolean(), nullable=False, server_default='1')
creation_date = db.Column(db.DateTime)
crl_expire_after = db.Column(db.DateTime)
content = db.Column(db.Text)
buf = ''
if os.path.exists(db_path):
raise ValueError('%r exists, not restoring.' % (db_path, ))
c = sqlite3.connect(db_path, isolation_level=None).cursor()
for chunk in restorator:
statement_list = (buf + chunk).split('\0')
buf = statement_list.pop()
for statement in statement_list:
c.execute((statement).decode('utf-8'))
if buf:
raise ValueError('Short read, backup truncated ?')
{% 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/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
Test suite
"""
from __future__ import absolute_import
from cStringIO import StringIO
import datetime
import errno
import glob
import ipaddress
import os
import multiprocessing
import random
import shutil
import socket
import sqlite3
import sys
import tempfile
import time
import urlparse
import unittest
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from caucase import cli
from caucase.client import CaucaseError
from caucase import http
from caucase import utils
from caucase import exceptions
from caucase import wsgi
from caucase.storage import SQLite3Storage
_cryptography_backend = default_backend()
NOT_CAUCASE_OID = '2.25.285541874270823339875695650038637483518'
def canConnect(address):
"""
Returns True if a connection can be established to given address, False
otherwise.
"""
try:
socket.create_connection(address)
except socket.error, e:
if e.errno == errno.ECONNREFUSED:
return False
raise
return True
def retry(callback, try_count=10, try_delay=0.1):
"""
Poll <callback> every <try_delay> for <try_count> times or until it returns
a true value.
Always returns the value returned by latest callback invocation.
"""
for _ in xrange(try_count):
result = callback()
if result:
return result
time.sleep(try_delay)
class CaucaseTest(unittest.TestCase):
"""
Test a complete caucase setup: spawn a caucase-http server on CAUCASE_NETLOC
and use caucase-cli to access it.
"""
_server = None
def setUp(self):
"""
Prepare test data directory and file paths, and start caucased as most
tests will need to interact with it.
"""
self._data_dir = data_dir = tempfile.mkdtemp(prefix='caucase_test_')
self._client_dir = client_dir = os.path.join(data_dir, 'client')
os.mkdir(client_dir)
# pylint: disable=bad-whitespace
self._client_ca_crt = os.path.join(client_dir, 'cas.crt.pem')
self._client_user_ca_crt = os.path.join(client_dir, 'cau.crt.pem')
self._client_crl = os.path.join(client_dir, 'cas.crl.pem')
self._client_user_crl = os.path.join(client_dir, 'cau.crl.pem')
# pylint: enable=bad-whitespace
self._server_dir = server_dir = os.path.join(data_dir, 'server')
os.mkdir(server_dir)
# pylint: disable=bad-whitespace
self._server_db = os.path.join(server_dir, 'caucase.sqlite')
self._server_key = os.path.join(server_dir, 'server.key.pem')
self._server_backup_path = os.path.join(server_dir, 'backup')
# pylint: enable=bad-whitespace
os.mkdir(self._server_backup_path)
self._server_netloc = netloc = os.getenv('CAUCASE_NETLOC', 'localhost:8000')
self._caucase_url = 'http://' + netloc
parsed_url = urlparse.urlparse(self._caucase_url)
self.assertFalse(
canConnect((parsed_url.hostname, parsed_url.port)),
'Something else is already listening on %r, define CAUCASE_NETLOC '
'environment variable with a different ip/port' % (netloc, ),
)
self._startServer()
def tearDown(self):
"""
Stop any running caucased and delete all test data files.
"""
if self._server.is_alive():
self._stopServer()
else:
print 'Server exited with status: %s' % (self._server.exitcode, )
shutil.rmtree(self._data_dir)
def _restoreServer(
self,
backup_path,
key_path,
new_csr_path,
new_key_path,
try_count=10,
):
"""
Start caucased in its special --restore-backup mode. It will exit once
done.
Returns its exit status.
"""
server = multiprocessing.Process(
target=http.main,
kwargs={
'argv': (
'--db', self._server_db,
'--server-key', self._server_key,
'--netloc', self._server_netloc,
#'--threshold', '31',
#'--key-len', '2048',
'--backup-directory', self._server_backup_path,
'--restore-backup',
backup_path,
key_path,
new_csr_path,
new_key_path,
),
}
)
server.daemon = True
server.start()
# Must exit after a (short) while
if not retry(lambda: not server.is_alive(), try_count=try_count):
raise AssertionError('Backup restoration took more than %i second' % (
try_count / 10,
))
return server.exitcode
def _startServer(self, *argv):
"""
Start caucased server
"""
self._server = server = multiprocessing.Process(
target=http.main,
kwargs={
'argv': (
'--db', self._server_db,
'--server-key', self._server_key,
'--netloc', self._server_netloc,
#'--threshold', '31',
#'--key-len', '2048',
) + argv,
}
)
server.daemon = True
server.start()
parsed_url = urlparse.urlparse(self._caucase_url)
if not retry(
lambda: canConnect((parsed_url.hostname, parsed_url.port)),
):
self._stopServer()
raise AssertionError('Could not connect to %r after 1 second' % (
self._caucase_url,
))
def _stopServer(self):
"""
Stop a running caucased server
"""
server = self._server
server.terminate()
server.join(.1)
if server.is_alive():
# Sometimes, server survives to a SIGTERM. Maybe an effect of it being
# multi-threaded, or something in python which would catch SystemExit ?
# It does typically succeed on second try, so just do that.
server.terminate()
server.join(.1)
if server.is_alive():
raise ValueError('pid %i does not wish to die' % (server.pid, ))
def _runClient(self, *argv):
"""
Run client with given arguments.
Returns stdout.
"""
orig_stdout = sys.stdout
sys.stdout = stdout = StringIO()
try:
cli.main(
argv=(
'--ca-url', self._caucase_url,
'--ca-crt', self._client_ca_crt,
'--user-ca-crt', self._client_user_ca_crt,
'--crl', self._client_crl,
'--user-crl', self._client_user_crl,
) + argv,
)
except SystemExit:
pass
finally:
sys.stdout = orig_stdout
return stdout.getvalue()
@staticmethod
def _setCertificateRemainingLifeTime(key, crt, delta):
"""
Re-sign <crt> with <key>, shifting both its not_valid_before and
not_valid_after dates so that its remaining validity period
becomes <delta> and its validity span stays unchanged.
"""
new_not_valid_after_date = datetime.datetime.utcnow() + delta
return x509.CertificateBuilder(
subject_name=crt.subject,
issuer_name=crt.issuer,
not_valid_before=new_not_valid_after_date - (
crt.not_valid_after - crt.not_valid_before
),
not_valid_after=new_not_valid_after_date,
serial_number=crt.serial_number,
public_key=crt.public_key(),
extensions=crt.extensions,
).sign(
private_key=key,
algorithm=crt.signature_hash_algorithm,
backend=_cryptography_backend,
)
def _setCACertificateRemainingLifeTime(self, mode, serial, delta):
"""
Find the CA certificate with <serial> in caucase <mode> ("service"
or "user") and pass it to _setCertificateRemainingLifeTime with <delta>.
"""
int(serial) # Must already be an integer
prefix = {
'user': 'cau',
'service': 'cas',
}[mode]
db = sqlite3.connect(self._server_db)
db.row_factory = sqlite3.Row
with db:
c = db.cursor()
c.execute(
'SELECT rowid, key, crt FROM ' + prefix + 'ca',
)
while True:
row = c.fetchone()
if row is None:
raise Exception('CA with serial %r not found' % (serial, ))
crt = utils.load_ca_certificate(row['crt'].encode('ascii'))
if crt.serial_number == serial:
new_crt = self._setCertificateRemainingLifeTime(
key=utils.load_privatekey(row['key'].encode('ascii')),
crt=crt,
delta=delta,
)
new_crt_pem = utils.dump_certificate(new_crt)
c.execute(
'UPDATE ' + prefix + 'ca SET '
'expiration_date=?, crt=? '
'WHERE rowid=?',
(
utils.datetime2timestamp(new_crt.not_valid_after),
new_crt_pem,
row['rowid'],
),
)
return new_crt_pem
def _getBaseName(self):
"""
Returns a random file name, prefixed by data directory.
"""
return os.path.join(
self._data_dir,
str(random.getrandbits(32)),
)
@staticmethod
def _createPrivateKey(basename, key_len=2048):
"""
Create a private key and store it to file.
"""
name = basename + '.key.pem'
assert not os.path.exists(name)
with open(name, 'w') as key_file:
key_file.write(utils.dump_privatekey(
utils.generatePrivateKey(key_len=key_len),
))
return name
@staticmethod
def _getBasicCSRBuilder():
"""
Initiate a minimal CSR builder.
"""
return x509.CertificateSigningRequestBuilder(
subject_name=x509.Name([
x509.NameAttribute(
oid=x509.oid.NameOID.COMMON_NAME,
value=u'test',
),
]),
)
@staticmethod
def _finalizeCSR(basename, key_path, csr_builder):
"""
Sign, serialise and store given CSR Builder to file.
"""
name = basename + '.csr.pem'
assert not os.path.exists(name)
with open(name, 'w') as csr_file:
csr_file.write(
utils.dump_certificate_request(
csr_builder.sign(
private_key=utils.load_privatekey(utils.getKey(key_path)),
algorithm=utils.DEFAULT_DIGEST_CLASS(),
backend=_cryptography_backend,
),
),
)
return name
def _createBasicCSR(self, basename, key_path):
"""
Creates a basic CSR and returns its path.
"""
return self._finalizeCSR(
basename,
key_path,
self._getBasicCSRBuilder(),
)
def _createFirstUser(self, add_extensions=False):
"""
Create first user, whose CSR is automatically signed.
"""
basename = self._getBaseName()
user_key_path = self._createPrivateKey(basename)
csr_builder = self._getBasicCSRBuilder()
if add_extensions:
csr_builder = csr_builder.add_extension(
x509.CertificatePolicies([
x509.PolicyInformation(
x509.oid.ObjectIdentifier(NOT_CAUCASE_OID),
None,
)
]),
critical=False,
)
csr_path = self._finalizeCSR(
basename,
user_key_path,
csr_builder,
)
out, = self._runClient(
'--mode', 'user',
'--send-csr', csr_path,
).splitlines()
csr_id, csr_path_out = out.split()
# Sanity check output
self.assertEqual(csr_path, csr_path_out)
int(csr_id)
self.assertRaises(TypeError, utils.getCert, user_key_path)
self._runClient(
'--mode', 'user',
'--get-crt', csr_id, user_key_path,
)
# Does not raise anymore, we have a certificate
utils.getCert(user_key_path)
return user_key_path
def _createAndApproveCertificate(self, user_key_path, mode):
"""
Create a CSR, submit it, approve it and retrieve the certificate.
"""
basename = self._getBaseName()
key_path = self._createPrivateKey(basename)
csr_path = self._createBasicCSR(basename, key_path)
out, = self._runClient(
'--mode', mode,
'--send-csr', csr_path,
).splitlines()
csr_id, csr_path_out = out.split()
# Sanity check output
self.assertEqual(csr_path, csr_path_out)
int(csr_id)
self.assertRaises(TypeError, utils.getCert, key_path)
out = self._runClient(
'--mode', mode,
'--get-crt', csr_id, key_path,
).splitlines()
self.assertRaises(TypeError, utils.getCert, key_path)
self.assertEqual([csr_id + ' CSR still pending'], out)
csr2_path = csr_path + '.2'
self._runClient(
'--mode', mode,
'--get-csr', csr_id, csr2_path,
)
self.assertEqual(open(csr_path).read(), open(csr2_path).read())
# Sign using user cert
# Note: assuming user does not know the csr_id and keeps their own copy of
# issued certificates.
out = self._runClient(
'--mode', mode,
'--user-key', user_key_path,
'--list-csr',
).splitlines()
self.assertEqual([csr_id], [x.split(None, 1)[0] for x in out[2:-1]])
self.assertRaises(
CaucaseError,
self._runClient,
'--mode', mode,
'--user-key', user_key_path,
'--sign-csr', str(int(csr_id) + 1),
)
out = self._runClient(
'--mode', mode,
'--user-key', user_key_path,
'--sign-csr', csr_id,
)
self.assertRaises(
CaucaseError,
self._runClient,
'--mode', mode,
'--user-key', user_key_path,
'--sign-csr', csr_id,
)
# Now requester can get their certificate
out, = self._runClient(
'--mode', mode,
'--get-crt', csr_id, key_path,
).splitlines()
# Does not raise anymore, we have a certificate
utils.getCert(user_key_path)
return key_path
def testBasicUsage(self):
"""
Everybody plays by the rules (which includes trying to access when
revoked).
"""
self.assertFalse(os.path.exists(self._client_ca_crt))
self.assertFalse(os.path.exists(self._client_crl))
self.assertFalse(os.path.exists(self._client_user_ca_crt))
self.assertFalse(os.path.exists(self._client_user_crl))
# Running client creates CAS files (service CA & service CRL)
self._runClient()
self.assertTrue(os.path.exists(self._client_ca_crt))
self.assertTrue(os.path.exists(self._client_crl))
self.assertFalse(os.path.exists(self._client_user_ca_crt))
self.assertFalse(os.path.exists(self._client_user_crl))
# Running in "user" mode also created the CAU CA, but not the CAU CRL
self._runClient('--mode', 'user')
self.assertTrue(os.path.exists(self._client_ca_crt))
self.assertTrue(os.path.exists(self._client_crl))
self.assertTrue(os.path.exists(self._client_user_ca_crt))
self.assertFalse(os.path.exists(self._client_user_crl))
cas_crt_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_ca_crt)
]
cau_crt_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_user_ca_crt)
]
# No CA renewal happened yet
self.assertEqual(len(cas_crt_list), 1)
self.assertEqual(len(cau_crt_list), 1)
# Get a user key pair
user_key_path = self._createFirstUser()
# It must have been auto-signed
self.assertTrue(utils.isCertificateAutoSigned(utils.load_certificate(
# utils.getCert(user_key_path) does not raise anymore
utils.getCert(user_key_path),
cau_crt_list,
None,
)))
# Get a not-auto-approved service crt (the first auto-approved one was for
# the http server itself)
service_key = self._createAndApproveCertificate(
user_key_path,
'service',
)
self.assertFalse(utils.isCertificateAutoSigned(utils.load_certificate(
utils.getCert(service_key),
cas_crt_list,
None,
)))
# Get a not-auto-approved user crt
user2_key_path = self._createAndApproveCertificate(
user_key_path,
'user',
)
self.assertFalse(utils.isCertificateAutoSigned(utils.load_certificate(
utils.getCert(user2_key_path),
cau_crt_list,
None,
)))
# It can itself sign certificates...
service2_key_path = self._createAndApproveCertificate(
user2_key_path,
'service',
)
user3_key_path = self._createAndApproveCertificate(
user2_key_path,
'user',
)
self._runClient(
'--user-key', user2_key_path,
'--list-csr',
)
self._runClient(
'--mode', 'user',
'--user-key', user2_key_path,
'--list-csr',
)
# ...until it gets revoked
self._runClient(
'--user-key', user_key_path,
'--mode', 'user',
'--revoke-other-crt', user2_key_path,
'--update-user',
)
self.assertRaises(
CaucaseError,
self._createAndApproveCertificate,
user2_key_path,
'service',
)
self.assertRaises(
CaucaseError,
self._createAndApproveCertificate,
user2_key_path,
'user',
)
self.assertRaises(
CaucaseError,
self._runClient,
'--user-key', user2_key_path,
'--list-csr',
)
self.assertRaises(
CaucaseError,
self._runClient,
'--mode', 'user',
'--user-key', user2_key_path,
'--list-csr',
)
# But the user it approved still works...
self._runClient(
'--user-key', user3_key_path,
'--list-csr',
)
# ...until it revokes itself
self._runClient(
'--mode', 'user',
'--user-key', user3_key_path,
'--revoke-serial', str(
utils.load_certificate(
utils.getCert(user3_key_path),
cau_crt_list,
None,
).serial_number,
)
)
self.assertRaises(
CaucaseError,
self._runClient,
'--user-key', user3_key_path,
'--list-csr',
)
# And the service it approved works too
service2_crt_before, service2_key_before, _ = utils.getKeyPair(
service2_key_path,
)
self._runClient(
# 100 days is longer than certificate life, so it will be immediately
# renewed.
'--threshold', '100',
'--renew-crt', service2_key_path, '',
)
service2_crt_after, service2_key_after, _ = utils.getKeyPair(
service2_key_path,
)
# Certificate and key were renewed...
self.assertNotEqual(service2_crt_before, service2_crt_after)
self.assertNotEqual(service2_key_before, service2_key_after)
# ...and not just swapped
self.assertNotEqual(service2_crt_before, service2_key_after)
self.assertNotEqual(service2_key_before, service2_crt_after)
# It can revoke itself...
self._runClient(
'--revoke-crt', service2_key_path, '',
)
# ...and then it cannot renew itself any more...
self.assertRaises(
CaucaseError,
self._runClient,
'--threshold', '100',
'--renew-crt', service2_key_path, '',
)
service2_crt_after2, service2_key_after2, _ = utils.getKeyPair(
service2_key_path,
)
# and crt & key did not change
self.assertEqual(service2_crt_after, service2_crt_after2)
self.assertEqual(service2_key_after, service2_key_after2)
# revoking again one's own certificate fails
self.assertRaises(
CaucaseError,
self._runClient,
'--revoke-crt', service2_key_path, '',
)
# as does revoking with an authenticated user
self.assertRaises(
CaucaseError,
self._runClient,
'--user-key', user_key_path,
'--revoke-other-crt', service2_key_path,
)
# and revoking by serial
self.assertRaises(
CaucaseError,
self._runClient,
'--user-key', user_key_path,
'--revoke-serial', str(
utils.load_certificate(
utils.getCert(service2_key_path),
cas_crt_list,
None,
).serial_number,
),
)
# Rejecting a CSR
basename = self._getBaseName()
key_path = self._createPrivateKey(basename)
csr_path = self._createBasicCSR(basename, key_path)
out, = self._runClient(
'--send-csr', csr_path,
).splitlines()
csr_id, csr_path_out = out.split()
# Sanity check output
self.assertEqual(csr_path, csr_path_out)
int(csr_id)
self.assertRaises(TypeError, utils.getCert, key_path)
out = self._runClient(
'--get-crt', csr_id, key_path,
).splitlines()
self.assertRaises(TypeError, utils.getCert, key_path)
self.assertEqual([csr_id + ' CSR still pending'], out)
out = self._runClient(
'--user-key', user_key_path,
'--reject-csr', csr_id,
).splitlines()
# Re-rejecting fails
self.assertRaises(
CaucaseError,
self._runClient,
'--user-key', user_key_path,
'--reject-csr', csr_id,
)
# like rejecting a non-existing crt
self.assertRaises(
CaucaseError,
self._runClient,
'--user-key', user_key_path,
'--reject-csr', str(int(csr_id) + 1),
)
out = self._runClient(
'--get-crt', csr_id, key_path,
).splitlines()
self.assertRaises(TypeError, utils.getCert, key_path)
self.assertEqual([
csr_id + ' not found - either csr id has a typo or CSR was rejected'
], out)
def testUpdateUser(self):
"""
Verify that CAU certificate and revocation list are created when the
(rarely needed) --update-user option is given.
"""
self.assertFalse(os.path.exists(self._client_ca_crt))
self.assertFalse(os.path.exists(self._client_crl))
self.assertFalse(os.path.exists(self._client_user_ca_crt))
self.assertFalse(os.path.exists(self._client_user_crl))
self._runClient('--update-user')
self.assertTrue(os.path.exists(self._client_ca_crt))
self.assertTrue(os.path.exists(self._client_crl))
self.assertTrue(os.path.exists(self._client_user_ca_crt))
self.assertTrue(os.path.exists(self._client_user_crl))
def testMaxCSR(self):
"""
Verify that the number of pending CSR is properly constrained.
"""
csr_list = []
def assertCanSend(count):
"""
Check that caucased accepts <count> CSR, and rejects the next one.
Appends the data of created CSRs (csr_id and csr_path) to csr_list.
"""
for _ in xrange(count):
basename = self._getBaseName()
csr_path = self._createBasicCSR(
basename,
self._createPrivateKey(basename),
)
out, = self._runClient('--send-csr', csr_path).splitlines()
csr_id, _ = out.split()
csr_list.append((csr_id, csr_path))
basename = self._getBaseName()
bad_csr_path = self._createBasicCSR(
basename,
self._createPrivateKey(basename),
)
self.assertRaises(
CaucaseError,
self._runClient,
'--send-csr',
bad_csr_path,
)
user_key_path = self._createFirstUser()
self._stopServer()
self._startServer(
'--service-max-csr', '5',
)
assertCanSend(5)
# But resubmitting one of the accepted ones is still fine
_, csr_path = csr_list[0]
self._runClient('--send-csr', csr_path)
# Accepted certificates do not count towards the total, even if not
# retrieved by owner
csr_id, csr_path = csr_list.pop()
self._runClient(
'--user-key', user_key_path,
'--sign-csr', csr_id,
)
assertCanSend(1)
# Rejected certificates do not count towards the total.
csr_id, _ = csr_list.pop()
self._runClient(
'--user-key', user_key_path,
'--reject-csr', csr_id,
)
assertCanSend(1)
def testLockAutoSignAmount(self):
"""
Verify that auto-approve limit freezing works.
"""
self._stopServer()
self._startServer(
'--user-auto-approve-count', '2',
'--lock-auto-approve-count',
)
self._stopServer()
self._startServer(
'--user-auto-approve-count', '3',
)
self._createFirstUser()
self._createFirstUser()
self.assertRaises(TypeError, self._createFirstUser)
self._stopServer()
self._startServer(
'--user-auto-approve-count', '3',
'--lock-auto-approve-count',
)
self.assertRaises(TypeError, self._createFirstUser)
def testCSRFiltering(self):
"""
Verify that requester cannot get any extension or extension value they
ask for. Caucase has to protect itself to be trustworthy, but also to let
some liberty to requester.
"""
def checkCRT(key_path):
"""
Verify key_path to contain exactly one certificate, itself containing
all expected extensions.
"""
crt = utils.load_certificate(
utils.getCert(key_path),
cas_crt_list,
None,
)
# CA-only extension, must not be present in certificate
self.assertRaises(
x509.ExtensionNotFound,
crt.extensions.get_extension_for_class,
x509.NameConstraints,
)
for expected_value in [
expected_key_usage,
expected_extended_usage,
expected_alt_name,
expected_policies,
expected_basic_constraints,
]:
extension = crt.extensions.get_extension_for_class(
expected_value.__class__,
)
self.assertEqual(
extension.value,
expected_value,
)
self.assertTrue(extension.critical)
requested_key_usage = x509.KeyUsage(
# pylint: disable=bad-whitespace
digital_signature =True,
content_commitment=True,
key_encipherment =True,
data_encipherment =True,
key_agreement =True,
key_cert_sign =True,
crl_sign =True,
encipher_only =True,
decipher_only =False,
# pylint: enable=bad-whitespace
)
expected_key_usage = x509.KeyUsage(
# pylint: disable=bad-whitespace
digital_signature =True,
content_commitment=True,
key_encipherment =True,
data_encipherment =True,
key_agreement =True,
key_cert_sign =False,
crl_sign =False,
encipher_only =True,
decipher_only =False,
# pylint: enable=bad-whitespace
)
requested_extended_usage = x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING,
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
])
expected_extended_usage = x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
])
requested_alt_name = expected_alt_name = x509.SubjectAlternativeName([
x509.RFC822Name('nobody@example.com'),
x509.DNSName('example.com'),
x509.UniformResourceIdentifier('https://example.com/a/b/c'),
x509.IPAddress(ipaddress.IPv4Address(u'127.0.0.1')),
x509.IPAddress(ipaddress.IPv6Address(u'::1')),
x509.IPAddress(ipaddress.IPv4Network(u'127.0.0.0/8')),
x509.IPAddress(ipaddress.IPv6Network(u'::/64')),
])
requested_policies = x509.CertificatePolicies([
x509.PolicyInformation(
x509.oid.ObjectIdentifier(utils.CAUCASE_OID_RESERVED),
None,
),
x509.PolicyInformation(
x509.oid.ObjectIdentifier(NOT_CAUCASE_OID),
None,
),
])
expected_policies = x509.CertificatePolicies([
x509.PolicyInformation(
x509.oid.ObjectIdentifier(NOT_CAUCASE_OID),
None,
),
])
requested_basic_constraints = x509.BasicConstraints(
ca=True,
path_length=42,
)
expected_basic_constraints = x509.BasicConstraints(
ca=False,
path_length=None,
)
# Check stored CSR filtering
user_key_path = self._createFirstUser(add_extensions=True)
basename = self._getBaseName()
key_path = self._createPrivateKey(basename)
requested_csr_path = self._finalizeCSR(
basename,
key_path,
self._getBasicCSRBuilder(
).add_extension(requested_key_usage, critical=True,
).add_extension(requested_extended_usage, critical=True,
).add_extension(requested_alt_name, critical=True,
).add_extension(requested_policies, critical=True,
).add_extension(requested_basic_constraints, critical=True,
).add_extension(
x509.NameConstraints([x509.DNSName('com')], None),
critical=True,
),
)
out, = self._runClient(
'--send-csr', requested_csr_path,
).splitlines()
csr_id, _ = out.split()
int(csr_id)
self._runClient(
'--user-key', user_key_path,
'--sign-csr', csr_id,
)
self._runClient(
'--get-crt', csr_id, key_path,
)
cas_crt_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_ca_crt)
]
cau_crt_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_user_ca_crt)
]
checkCRT(key_path)
# Check renewed CRT filtering does not alter clean signed certificate
# content (especially, caucase auto-signed flag must not appear).
before_key = open(key_path).read()
self._runClient(
'--threshold', '100',
'--renew-crt', key_path, '',
)
after_key = open(key_path).read()
assert before_key != after_key
checkCRT(key_path)
# Check content of auto-issued user certificate
user_crt = utils.load_certificate(
utils.getCert(user_key_path),
cau_crt_list,
None,
)
user_certificate_policies = user_crt.extensions.get_extension_for_class(
x509.CertificatePolicies,
)
self.assertEqual(
user_certificate_policies.value,
x509.CertificatePolicies([
x509.PolicyInformation(
x509.oid.ObjectIdentifier(NOT_CAUCASE_OID),
None,
),
utils.CAUCASE_POLICY_INFORMATION_AUTO_SIGNED,
]),
)
self.assertFalse(user_certificate_policies.critical)
# Check template CSR: must be taken into account, but it also gets
# filtered.
basename2 = self._getBaseName()
key_path2 = self._createPrivateKey(basename2)
out, = self._runClient(
'--send-csr', self._finalizeCSR(
basename2,
key_path2,
self._getBasicCSRBuilder(),
),
).splitlines()
csr_id, _ = out.split()
int(csr_id)
self._runClient(
'--user-key', user_key_path,
'--sign-csr-with', csr_id, requested_csr_path,
)
self._runClient(
'--get-crt', csr_id, key_path2,
)
checkCRT(key_path2)
def testCACertRenewal(self):
"""
Exercise CA certificate rollout procedure.
"""
user_key_path = self._createFirstUser()
cau_crt, = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_user_ca_crt)
]
self._stopServer()
# CA expires in 100 days: longer than one certificate life,
# but shorter than two. A new CA must be generated and distributed,
# but not used for new signatures yet.
new_cau_crt_pem = self._setCACertificateRemainingLifeTime(
'user',
cau_crt.serial_number,
datetime.timedelta(100, 0),
)
# As we will use this crt as trust anchor, we must make the client believe
# it knew it all along.
with open(self._client_user_ca_crt, 'w') as client_user_ca_crt_file:
client_user_ca_crt_file.write(new_cau_crt_pem)
self._startServer()
new_user_key = self._createAndApproveCertificate(
user_key_path,
'user',
)
# Must not raise: we are signed with the "old" ca.
utils.load_certificate(
utils.getCert(new_user_key),
[cau_crt],
None,
)
# We must now know the new CA
cau_crt_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_user_ca_crt)
]
new_cau_crt, = [
x for x in cau_crt_list
if x.serial_number != cau_crt.serial_number
]
self._stopServer()
# New CA now exists for 100 days: longer than one certificate life.
# It may (must) be used for new signatures.
self._setCACertificateRemainingLifeTime(
'user',
new_cau_crt.serial_number,
new_cau_crt.not_valid_after - new_cau_crt.not_valid_before -
datetime.timedelta(100, 0),
)
self._startServer()
self._runClient(
'--mode', 'user',
# 100 days is longer than certificate life, so it will be immediately
# renewed.
'--threshold', '100',
'--renew-crt', new_user_key, '',
)
self.assertRaises(
exceptions.CertificateVerificationError,
utils.load_certificate,
utils.getCert(new_user_key),
[cau_crt],
None,
)
utils.load_certificate(
utils.getCert(new_user_key),
cau_crt_list,
None,
)
def testCaucasedCertRenewal(self):
"""
Exercise caucased internal certificate renewal procedure.
"""
user_key_path = self._createFirstUser()
self._stopServer()
# If server certificate is deleted, it gets re-created, even it CAS
# reached its certificate auto-approval limit.
os.unlink(self._server_key)
self._startServer()
if not retry(lambda: os.path.exists(self._server_key)):
raise AssertionError('%r was not recreated within 1 second' % (
self._server_key,
))
# But user still trusts the server
self._runClient(
'--mode', 'user',
# 100 days is longer than certificate life, so it will be immediately
# renewed.
'--threshold', '100',
'--renew-crt', user_key_path, '',
)
# Server certificate will expire in 20 days, the key must be renewed
# (but we have to peek at CAS private key, cover your eyes)
(cas_key, ), = sqlite3.connect(
self._server_db,
).cursor().execute(
'SELECT key FROM casca',
).fetchall()
self._stopServer()
crt_pem, key_pem, _ = reference_key_pair_result = utils.getKeyPair(
self._server_key,
)
with open(self._server_key, 'w') as server_key_file:
server_key_file.write(key_pem)
server_key_file.write(utils.dump_certificate(
self._setCertificateRemainingLifeTime(
key=utils.load_privatekey(cas_key.encode('ascii')),
crt=utils.load_certificate(
crt_pem,
[
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_ca_crt)
],
None,
),
delta=datetime.timedelta(20, 0)
)
))
self._startServer()
if not retry(
lambda: utils.getKeyPair(self._server_key) != reference_key_pair_result,
):
raise AssertionError('Server did not renew its key pair within 1 second')
# But user still trusts the server
self._runClient(
'--mode', 'user',
# 100 days is longer than certificate life, so it will be immediately
# renewed.
'--threshold', '100',
'--renew-crt', user_key_path, '',
)
def testWSGI(self):
"""
Test wsgi class reaction to malformed requests.
For tests which are not accessible through the client module (as it tries
to only produce valid requests).
"""
self._runClient('--mode', 'user', '--update-user')
cau_list = [
utils.load_ca_certificate(x)
for x in utils.getCertList(self._client_user_ca_crt)
]
cau_crl = open(self._client_user_crl).read()
class DummyCAU(object):
"""
Mock CAU.
"""
def getCACertificateList(self):
"""
Return cau ca list.
"""
return cau_list
def getCertificateRevocationList(self):
"""
Return cau crl.
"""
return cau_crl
@staticmethod
def appendCertificateSigningRequest(_):
"""
Raise to exercise the "unexpected exception" code path in WSGI.
"""
raise ValueError('Some generic exception')
application = wsgi.Application(DummyCAU(), None)
def request(environ):
"""
Non-standard shorthand for invoking the WSGI application.
"""
start_response_list = []
body = list(application(
environ,
lambda status, header_list: start_response_list.append(
(status, header_list),
),
))
# pylint: disable=unbalanced-tuple-unpacking
(status, header_list), = start_response_list
# pylint: enable=unbalanced-tuple-unpacking
status, reason = status.split(' ', 1)
return int(status), reason, header_list, ''.join(body)
UNAUTHORISED_STATUS = 404
self.assertEqual(request({
'PATH_INFO': '/',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/foo/bar',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/__init__',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/does_not_exist',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/crl/123',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/crl',
'REQUEST_METHOD': 'PUT',
})[0], 405)
self.assertEqual(request({
'PATH_INFO': '/cau/csr/123/456',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/csr/123',
'REQUEST_METHOD': 'POST',
})[0], 405)
self.assertEqual(request({
'PATH_INFO': '/cau/csr/a',
'REQUEST_METHOD': 'GET',
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/csr',
'REQUEST_METHOD': 'GET',
})[0], UNAUTHORISED_STATUS)
self.assertEqual(request({
'PATH_INFO': '/cau/csr/123',
'REQUEST_METHOD': 'PUT',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/csr',
'REQUEST_METHOD': 'PUT',
'wsgi.input': StringIO(),
})[0], 500)
self.assertEqual(request({
'PATH_INFO': '/cau/csr',
'REQUEST_METHOD': 'DELETE',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/csr/123/456',
'REQUEST_METHOD': 'DELETE',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/csr/123',
'REQUEST_METHOD': 'DELETE',
})[0], UNAUTHORISED_STATUS)
self.assertEqual(request({
'PATH_INFO': '/cau/crt',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/123/456',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/a',
'REQUEST_METHOD': 'GET',
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/renew',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'text/plain',
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/renew',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'application/json',
'CONTENT_LENGTH': 'a',
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/renew',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'application/json',
'CONTENT_LENGTH': str(10 * 1024 * 1024 + 1),
})[0], 413)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/renew',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'application/json',
'wsgi.input': StringIO('{'),
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/revoke',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'application/json',
'wsgi.input': StringIO('{"digest": null}'),
})[0], UNAUTHORISED_STATUS)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/a',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'text/plain',
'wsgi.input': StringIO(''),
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/123',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'text/plain',
'wsgi.input': StringIO(''),
})[0], UNAUTHORISED_STATUS)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/123',
'REQUEST_METHOD': 'PUT',
'CONTENT_TYPE': 'text/plain',
'wsgi.input': StringIO('foo'),
})[0], 400)
self.assertEqual(request({
'PATH_INFO': '/cau/crt/123',
'REQUEST_METHOD': 'POST',
})[0], 405)
def testProbe(self):
"""
Exercise caucase-probe command.
"""
cli.probe([self._caucase_url])
def testBackup(self):
"""
Exercise backup generation and restoration.
"""
backup_glob = os.path.join(self._server_backup_path, '*.sql.caucased')
user_key_path = self._createFirstUser()
user2_key_path = self._createAndApproveCertificate(
user_key_path,
'user',
)
# user2 sacrifice their private key, and prepare its replacement
basename = self._getBaseName()
user2_new_key_path = self._createPrivateKey(basename)
user2_new_csr_path = self._createBasicCSR(basename, user2_new_key_path)
# Restart caucased to not have to wait for next backup deadline
self._stopServer()
# Note: backup could have triggered between first and second user's key
# creation. We need it to be signed by both keys, so delete any backup file
# which would exist at this point.
for backup_path in glob.glob(backup_glob):
os.unlink(backup_path)
before_backup = list(SQLite3Storage(
self._server_db,
table_prefix='cau',
).dumpIterator())
self._startServer('--backup-directory', self._server_backup_path)
backup_path_list = retry(lambda: glob.glob(backup_glob))
if not backup_path_list:
raise AssertionError('Backup file not created after 1 second')
backup_path, = backup_path_list
self._stopServer()
# Server must refuse to restore if the database still exists
self.assertNotEqual(
self._restoreServer(
backup_path,
user2_key_path,
user2_new_csr_path,
user2_new_key_path,
),
0,
)
os.unlink(self._server_db)
os.unlink(self._server_key)
# XXX: just for the coverage... Should check output
cli.key_id([
'--private-key', user_key_path, user2_key_path,
'--backup', backup_path,
])
self.assertEqual(
self._restoreServer(
backup_path,
user2_key_path,
user2_new_csr_path,
user2_new_key_path,
),
0,
)
after_restore = list(SQLite3Storage(
self._server_db,
table_prefix='cau',
).dumpIterator())
CRL_INSERT = 'INSERT INTO "caucrl" '
CRT_INSERT = 'INSERT INTO "caucrt" '
REV_INSERT = 'INSERT INTO "caurevoked" '
def filterBackup(backup, received_csr, expect_rev):
"""
Remove all lines which are know to differ between original batabase and
post-restoration database, so the rest (which must be the majority of the
database) can be tested to be equal.
"""
rev_found = not expect_rev
new_backup = []
crt_list = []
for row in backup:
if (
row == received_csr
) or row.startswith(CRL_INSERT):
continue
if row.startswith(CRT_INSERT):
crt_list.append(row)
continue
if row.startswith(REV_INSERT):
if rev_found:
raise AssertionError('Unexpected revocation found')
continue
new_backup.append(row)
return new_backup, crt_list
before_backup, before_crt_list = filterBackup(
before_backup,
'INSERT INTO "caucounter" VALUES(\'received_csr\',2);\x00',
False,
)
after_restore, after_crt_list = filterBackup(
after_restore,
'INSERT INTO "caucounter" VALUES(\'received_csr\',3);\x00',
True,
)
self.assertEqual(
len(set(after_crt_list).difference(before_crt_list)),
1,
)
self.assertEqual(
len(set(before_crt_list).difference(after_crt_list)),
0,
)
self.assertItemsEqual(before_backup, after_restore)
self._startServer('--backup-directory', self._server_backup_path)
# user2 got a new certificate matching their new key
utils.getKeyPair(user2_new_key_path)
# And user 1 must still work without key change
self._runClient(
'--user-key', user_key_path,
'--list-csr',
)
# Another backup can happen after restoration
self._stopServer()
for backup_path in glob.glob(backup_glob):
os.unlink(backup_path)
self._startServer('--backup-directory', self._server_backup_path)
backup_path_list = retry(lambda: glob.glob(backup_glob))
if not backup_path_list:
raise AssertionError('Backup file not created after 1 second')
backup_path, = glob.glob(backup_glob)
cli.key_id([
'--backup', backup_path,
])
# Now, push a lot of data to exercise chunked checksum in backup &
# restoration code
self._stopServer()
for backup_path in glob.glob(backup_glob):
os.unlink(backup_path)
db = sqlite3.connect(self._server_db)
db.row_factory = sqlite3.Row
with db:
c = db.cursor()
c.execute('CREATE TABLE bloat (bloat TEXT)')
bloat_query = 'INSERT INTO bloat VALUES (?)'
bloat_value = ('bloat' * 10240, )
for _ in xrange(1024):
c.execute(bloat_query, bloat_value)
db.close()
del db
self._startServer('--backup-directory', self._server_backup_path)
backup_path_list = retry(lambda: glob.glob(backup_glob), try_count=20)
if not backup_path_list:
raise AssertionError('Backup file not created after 2 second')
backup_path, = glob.glob(backup_glob)
cli.key_id([
'--backup', backup_path,
])
self._stopServer()
os.unlink(self._server_db)
os.unlink(self._server_key)
backup_path, = backup_path_list
# user2 sacrifice their private key, and prepare its replacement
basename = self._getBaseName()
user2_newnew_key_path = self._createPrivateKey(basename)
user2_newnew_csr_path = self._createBasicCSR(
basename,
user2_newnew_key_path,
)
user2_new_bare_key_path = user2_new_key_path + '.bare_key'
with open(user2_new_bare_key_path, 'w') as bare_key_file:
bare_key_file.write(utils.getKeyPair(user2_new_key_path)[1])
self.assertEqual(
self._restoreServer(
backup_path,
user2_new_bare_key_path,
user2_newnew_csr_path,
user2_newnew_key_path,
try_count=50,
),
0,
)
# 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/>.
# 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 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 os, time
import shutil
import tempfile
import unittest
import json
from datetime import datetime
from caucase import app, db
from OpenSSL import crypto, SSL
from caucase.exceptions import (NoStorage, NotFound, Found, BadSignature,
BadCertificateSigningRequest,
BadCertificate,
CertificateVerificationError,
ExpiredCertificate)
from caucase.ca import CertificateAuthority, DEFAULT_DIGEST_LIST, MIN_CA_RENEW_PERIOD
from caucase import utils
class CertificateAuthorityTest(unittest.TestCase):
def setUp(self):
self.ca_dir = tempfile.mkdtemp()
self.db_file = os.path.join(self.ca_dir, 'ca.db')
self.max_request_amount = 3
self.default_digest = "sha256"
self.crt_keep_time = 0
app.config.update(
DEBUG=True,
CSRF_ENABLED=True,
SECRET_KEY = 'This is an UNSECURE Secret. Please CHANGE THIS for production environments.',
TESTING=True,
SQLALCHEMY_DATABASE_URI='sqlite:///%s' % self.db_file
)
from caucase.storage import Storage
self._storage = Storage(db)
def make_ca(self, crt_life_time, auto_sign_csr=False):
return CertificateAuthority(
storage=self._storage,
ca_life_period=4,
ca_renew_period=2,
crt_life_time=crt_life_time,
crl_renew_period=0.1,
crl_base_url='http://crl.url.com',
ca_subject='/C=XX/ST=State/L=City/OU=OUnit/O=Company/CN=CAAuth/emailAddress=xx@example.com',
max_csr_amount=self.max_request_amount,
crt_keep_time=self.crt_keep_time,
auto_sign_csr=auto_sign_csr
)
def generateCSR(self, cn="toto.example.com", email="toto@example.com"):
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
req = crypto.X509Req()
subject = req.get_subject()
subject.CN = cn
subject.C = "CC"
subject.ST = "ST"
subject.L = "LOU"
subject.O = "OOU"
subject.OU = "OU"
subject.emailAddress = email
req.set_pubkey(key)
utils.X509Extension().setDefaultCsrExtensions(req)
req.sign(key, self.default_digest)
return (req, key)
def csr_tostring(self, csr):
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
def get_fake_cert_key(self):
cert_string = """-----BEGIN CERTIFICATE-----
MIID6DCCAtCgAwIBAwIBBDANBgkqhkiG9w0BAQsFADCBljEiMCAGA1UEAwwZVGhl
IENlcnRpZmljYXRlIEF1dGhvcml0eTELMAkGA1UEBhMCWFgxDjAMBgNVBAgMBVN0
YXRlMREwDwYDVQQKDAhDb21wYWdueTENMAsGA1UECwwEVW5pdDENMAsGA1UEBwwE
Q2l0eTEiMCAGCSqGSIb3DQEJARYTY2EuYXV0aEBleGFtcGxlLmNvbTAeFw0xNzA0
MTYyMTQyNDVaFw0xODA0MTYyMTQyNDVaMBsxGTAXBgNVBAMMEGRhbmRAZXhhbXBs
ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDrJNil62fkNm1j
UgEZ33hg5qYiLeNOEUgKaINCqhfQDuH7tTho+nRNxY2FSpv7ooyMckLYojNm+XEl
lUREE8hgIBiBWgiXazvoaxKFW2BIm7kpfxSC0t4pxwjehftm0Ny/nvms6SiE0ruL
3u+oUI9VVvgofra7mvhX3OZuZNb2QADaPnmyfB8VYGfzYAl0QgyFkrWzPflb/UXD
LdIP/niTpUHMgDTChQ+jf3tHm0pbsRZTxXISJY2+O5qpEgumW+Qcw5sKpjdfMYvH
hoM0IzMofBSfmQCZOjJRcisVLTASj5qN5+GFJi6Pz7oih203Ur8elq+iA0hRisA7
g37LNGXTAgMBAAGjgbowgbcwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl
blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFPOBwIfuP8mulzmk
MgC1VGLCkwEDMB8GA1UdIwQYMBaAFGXLZo/7QINvcowfmpUEllOAJ6I5MDwGA1Ud
HwQ1MDMwMaAvoC2GK2h0dHBzOi8vWzIwMDE6NjdjOjEyNTQ6ZTpjNDo6Yzc0OF06
ODAwOS9jcmwwDQYJKoZIhvcNAQELBQADggEBAKarSr7WKXznFjbLfbedrci9mtwo
TYVpOUt/nt6lCiJ2wTGQea/e4KQ3WRwlUUHCX/K+G4QEV8OeDIA4uXnx24fclj35
hCYQCaJfIM96Z+elYIisOX3eFZ9cuo4fkODnry+vQNkYuOn/mFe0sVxoBK+oqSl5
/tN7pTFB2CaSBnRrNHquEc6YFoglCjQW4fXzHdQCdx5B7oOg/yloIst2WagXbyvE
zQvWKm6jjB5/xdT5mpxHB/lanSZMGXFnITh8qXrlTxd/tSa3ic6+k1WC++5brUvE
MtldUnSV++fZh9C4xsXyi26ytr2KkwcJcXQbU2PF4iuV6L0eYO2xOgpDCoQ=
-----END CERTIFICATE-----"""
key_string = """-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDrJNil62fkNm1j
UgEZ33hg5qYiLeNOEUgKaINCqhfQDuH7tTho+nRNxY2FSpv7ooyMckLYojNm+XEl
lUREE8hgIBiBWgiXazvoaxKFW2BIm7kpfxSC0t4pxwjehftm0Ny/nvms6SiE0ruL
3u+oUI9VVvgofra7mvhX3OZuZNb2QADaPnmyfB8VYGfzYAl0QgyFkrWzPflb/UXD
LdIP/niTpUHMgDTChQ+jf3tHm0pbsRZTxXISJY2+O5qpEgumW+Qcw5sKpjdfMYvH
hoM0IzMofBSfmQCZOjJRcisVLTASj5qN5+GFJi6Pz7oih203Ur8elq+iA0hRisA7
g37LNGXTAgMBAAECggEASMRxSv9LekMhnN/OuWv/e7VE6kTbF9ifO6FWJXYvwlIo
utU87Le88ChXgE0zci6+YeQmLZYcZByDWEcWBh89Hgowqy7qg7lKo8UmySAa7r1K
Er5h4Y5R9AnFA9/gidPOzHns+AZ7ZIc2RLWr4qFzicxNJXL5J5twiPgyUy1fnHpg
Ktk3ccgtIe8IJNYS9hGW40X6DZfbiNqnUlGxS0Nsk7RQhEowcuAob7sBf2k6tSAx
qaaB3PYBwGsfgF6Zkq81/ZmzZPoD0vSLURAlglTWgGljqXOqXVnqUFbcyaZeKwqX
4b5MlQMZ09puOz5CGG0HhnTFmt+N1rU3Vsx74G5hWQKBgQD4wHt9WL6DQPQ2/i0l
o6afSd0+DV5HldHGCRt4aF3//bGU3OugNvHVK2ijXEUIDHcpvEW1vwjbqmkFI3Sn
wANCr8YAmu/51uYYyeUP4V35SKBtBBdhUvFOGE3MThJQusAdEYg65T9STQvmpRnJ
Yv5QRlX/jawEtS2H9pZo+WvZpQKBgQDx/tuiU2isBfOrT5MAVxtu/VEZvafwRcHe
0EmRCyW6+rHSA3o2/3f43t6x63qvvk6NYY9rSz/0TZkZ/Y3QihPNqGwGuuzSvsG+
yDfnv6YmtcnBPv2kXHwEeIsd9DdjqT4D3MIHGHo05cu9Ta7oTHVAo8OSQbEqkGwj
oYpuQTz4FwKBgQCxuo1A9OxByWHz/M1zDCdbviHGWTTYftH/5bfr4t3urmt4ChSM
R1WoUjiUJ7Pm2Uk2158TCSgiEvKwSjHqPUXXGtGk0w7M+l8yrOXt378OAnclDPxL
fECO5MyJQerSJWxoGIO2WN9SRVxQcfwnqIQ+BNMjIS0bu/uJHoU/AZ6uRQKBgQC5
FT9Oa5TG3NZ806OOwxCMVtpMYa2sKu4YSB27/VaiJ1MRWO+EWOedRHf2hC+VcmwJ
3fAfE7KaWy8Znb91G+YBiSr2CslOde8gx2laqk2dlbP1RQQhTUrc8IUWJ86lPq/b
rGAJpULyaj7lTiDUMoYLJjVSC0RBVawfpFGH+gVziQKBgQDI+dgmgFv3ULUUTBtP
iPwYrGFWmzcKwevPaIQqrsjJ0TY2INmQZi6pn4ogelUtcRMFjzpBGHrsxJM0RkSy
3A855c5gfp60XOQB3ab0OS0X5/gzDZHThN7wvspUFnZ9i6LhSEOHMEAxwSklCtPq
m4DpuP4nL0ixQJWZuV+qrx6Tow==
-----END PRIVATE KEY-----"""
return (cert_string, key_string)
def check_cert_equal(self, first, second):
if isinstance(first, crypto.X509):
first_string = crypto.dump_certificate(crypto.FILETYPE_PEM, first)
else:
first_string = first
second_string = crypto.dump_certificate(crypto.FILETYPE_PEM, second)
return first_string == second_string
def check_key_equal(self, first, second):
if isinstance(first, crypto.PKey):
first_string = crypto.dump_privatekey(crypto.FILETYPE_PEM, first)
else:
first_string = first
second_string = crypto.dump_privatekey(crypto.FILETYPE_PEM, second)
return first_string == second_string
def check_csr_equal(self, first, second):
if isinstance(first, crypto.X509Req):
first_string = crypto.dump_certificate_request(crypto.FILETYPE_PEM, first)
else:
first_string = first
second_string = crypto.dump_certificate_request(crypto.FILETYPE_PEM, second)
return first_string == second_string
def tearDown(self):
db.session.remove()
db.drop_all()
if os.path.exists(self.ca_dir):
shutil.rmtree(self.ca_dir)
def test_createCAKeyPair(self):
# initials keypair are generated when instanciating ca
ca = self.make_ca(160)
self.assertEquals(len(ca._ca_key_pairs_list), 1)
def renewCAKetPair(self):
ca = self.make_ca(2)
self.assertEquals(len(ca._ca_key_pairs_list), 1)
first = ca._ca_key_pairs_list
# No renew possible
self.assertFalse(ca.renewCAKeyPair())
time.sleep(5)
# ca Certificate should be renewed
self.assertTrue(ca.renewCAKeyPair())
self.assertEquals(len(ca._ca_key_pairs_list), 2)
self.assertTrue(self.check_cert_equal(ca._ca_key_pairs_list[0]['crt'], first['crt']))
self.assertTrue(self.check_key_equal(ca._ca_key_pairs_list[0]['key'], first['key']))
def test_getPendingCertificateRequest(self):
ca = self.make_ca(190)
csr, key = self.generateCSR()
csr_string = self.csr_tostring(csr)
csr_id = ca.createCertificateSigningRequest(csr_string)
stored = ca.getPendingCertificateRequest(csr_id)
self.assertEquals(csr_string, stored)
def test_deletePendingCertificateRequest(self):
ca = self.make_ca(190)
csr, key = self.generateCSR()
csr_string = self.csr_tostring(csr)
csr_id = ca.createCertificateSigningRequest(csr_string)
stored = ca.getPendingCertificateRequest(csr_id)
self.assertEquals(csr_string, stored)
ca.deletePendingCertificateRequest(csr_id)
with self.assertRaises(NotFound):
ca.getPendingCertificateRequest(csr_id)
def test_createCertificate(self):
ca = self.make_ca(190)
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
subj_dict = {'CN': 'toto.example.com',
'C': 'CC',
'ST': 'ST',
'L': 'LOU',
'O': 'OOU',
'OU': 'OU',
'emailAddress': 'toto@example.com'}
for attr in ['C', 'ST', 'L', 'OU', 'O', 'CN', 'emailAddress']:
self.assertEqual(getattr(x509.get_subject(), attr), subj_dict[attr])
with self.assertRaises(NotFound):
ca.getPendingCertificateRequest(csr_id)
def test_createCertificate_custom_subject(self):
ca = self.make_ca(190)
csr, key = self.generateCSR(cn="test certificate", email="some@test.com")
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
subject_dict = dict(CN="real cn", emailAddress="caucase@email.com")
# sign certificate but change subject
cert_id = ca.createCertificate(csr_id, subject_dict=subject_dict)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
self.assertEqual(x509.get_subject().CN, subject_dict['CN'])
self.assertEqual(x509.get_subject().emailAddress, subject_dict['emailAddress'])
# Others attributes are empty
for attr in ['C', 'ST', 'L', 'OU', 'O']:
self.assertEqual(getattr(x509.get_subject(), attr), None)
with self.assertRaises(NotFound):
ca.getPendingCertificateRequest(csr_id)
def test_createCertificate_custom_subject2(self):
ca = self.make_ca(190)
csr, key = self.generateCSR(cn="test certificate", email="some@test.com")
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
subject_dict = {'CN': 'some.site.com',
'C': 'FR',
'O': 'My Organisation',
'L': 'Localisation',
'OU': 'Organisation U',
'ST': 'State',
'emailAddress': 'toto@example.com'}
# sign certificate but change subject
cert_id = ca.createCertificate(csr_id, subject_dict=subject_dict)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
# certificate is still valid
self.assertTrue(utils.validateCertAndKey(x509, key))
# check that all attributes are set
for attr in ['C', 'ST', 'L', 'OU', 'O']:
self.assertEqual(getattr(x509.get_subject(), attr), subject_dict[attr])
with self.assertRaises(NotFound):
ca.getPendingCertificateRequest(csr_id)
def test_createCertificate_custom_subject_no_cn(self):
ca = self.make_ca(190)
csr, key = self.generateCSR(cn="test certificate", email="some@test.com")
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
subject_dict = dict(C="FR", emailAddress="caucase@email.com")
# CN is missing, will raise
with self.assertRaises(AttributeError):
ca.createCertificate(csr_id, subject_dict=subject_dict)
def test_getCAKeypairForCertificate(self):
csr, key = self.generateCSR()
ca = self.make_ca(3)
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
cert_keypair = ca._ca_key_pairs_list[0]
# 2 crt_life_time cycles + 1s
time.sleep(7)
# Create new CA keypair
self.assertTrue(ca.renewCAKeyPair())
# get keypair which should be used to renew this cert
calculated = ca.getCAKeypairForCertificate(x509)
self.assertTrue(self.check_cert_equal(cert_keypair['crt'], calculated['crt']))
self.assertTrue(self.check_key_equal(cert_keypair['key'], calculated['key']))
self.assertEquals(len(ca._ca_key_pairs_list), 2)
new_keypair = ca._ca_key_pairs_list[-1]
time.sleep(3)
# the first ca keypair cannot be used to renew cert (7+3 -12 = 2 < crt_life_time)
calculated = ca.getCAKeypairForCertificate(x509)
self.assertTrue(self.check_cert_equal(new_keypair['crt'], calculated['crt']))
self.assertTrue(self.check_key_equal(new_keypair['key'], calculated['key']))
def test_renewCertificate(self):
ca = self.make_ca(158)
csr, key = self.generateCSR()
csr_string = self.csr_tostring(csr)
csr_id = ca.createCertificateSigningRequest(csr_string)
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
payload = dict(
renew_csr=csr_string,
crt=cert
)
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key), [self.default_digest])
new_csr_id = ca.renew(wrapped)
new_cert_id = '%s.crt.pem' % new_csr_id[:-8]
new_cert = ca.getCertificate(new_cert_id)
new_x509 = crypto.load_certificate(crypto.FILETYPE_PEM, new_cert)
self.assertTrue(utils.validateCertAndKey(new_x509, key))
self.assertEquals(new_x509.get_subject().CN, x509.get_subject().CN)
# current certificate is not revoked
self.assertTrue(utils.validateCertAndKey(x509, key))
def test_renewCertificate_bad_cert(self):
ca = self.make_ca(158)
csr, key = self.generateCSR()
csr_string = self.csr_tostring(csr)
csr_id = ca.createCertificateSigningRequest(csr_string)
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
# second certificate
csr2, key2 = self.generateCSR()
csr_id2 = ca.createCertificateSigningRequest(self.csr_tostring(csr2))
cert_id2 = ca.createCertificate(csr_id2)
cert2 = ca.getCertificate(cert_id2)
x509_2 = crypto.load_certificate(crypto.FILETYPE_PEM, cert2)
self.assertTrue(utils.validateCertAndKey(x509_2, key2))
payload = dict(
renew_csr=csr_string,
crt=cert
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key2), [self.default_digest])
with self.assertRaises(BadSignature):
ca.renew(wrapped)
# payload with invalid PEM certificate
payload = dict(
renew_csr=csr_string,
crt="BAD PEM CERTIFICATE"
)
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key), [self.default_digest])
with self.assertRaises(BadSignature):
ca.renew(wrapped)
# payload with invalid PEM certificate request content
payload = dict(
renew_csr="BAD PEM CERTIFICATE REQUEST",
crt=cert
)
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key), [self.default_digest])
with self.assertRaises(BadCertificateSigningRequest):
ca.renew(wrapped)
# payload with Fake certificate
fcert, fkey = self.get_fake_cert_key()
payload = dict(
renew_csr=csr_string,
crt=cert
)
wrapped = utils.wrap(payload, fkey, [self.default_digest])
with self.assertRaises(BadSignature):
ca.renew(wrapped)
payload = dict(
renew_csr=csr_string,
crt=fcert
)
wrapped = utils.wrap(payload, fkey, [self.default_digest])
with self.assertRaises(BadCertificateSigningRequest):
# Fake certificate and renew_csr has not the same csr
ca.renew(wrapped)
def test_revokeCertificate(self):
ca = self.make_ca(158)
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
payload = dict(
reason='',
revoke_crt=cert
)
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key), [self.default_digest])
ca.revokeCertificate(wrapped)
with self.assertRaises(NotFound):
ca.getCertificate(cert_id)
revocation_list = self._storage.getRevocationList()
self.assertEquals(len(revocation_list), 1)
self.assertEquals(revocation_list[0].serial, utils.getSerialToInt(x509))
def test_revokeCertificate_expired(self):
ca = self.make_ca(2)
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
# wait until certificate expire
time.sleep(3)
payload = dict(
reason='',
revoke_crt=cert
)
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key), [self.default_digest])
with self.assertRaises(CertificateVerificationError):
# if certificate expire, verification fail
ca.revokeCertificate(wrapped)
def test_revokeCertificate_bad_cert(self):
ca = self.make_ca(158)
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
# second certificate
csr2, key2 = self.generateCSR()
csr_id2 = ca.createCertificateSigningRequest(self.csr_tostring(csr2))
cert_id2 = ca.createCertificate(csr_id2)
cert2 = ca.getCertificate(cert_id2)
x509_2 = crypto.load_certificate(crypto.FILETYPE_PEM, cert2)
self.assertTrue(utils.validateCertAndKey(x509_2, key2))
payload = dict(
reason="",
revoke_crt=cert
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key2), [self.default_digest])
with self.assertRaises(BadSignature):
ca.revokeCertificate(wrapped)
# payload with invalid PEM certificate
payload = dict(
reason="",
revoke_crt="BAD PEM CERTIFICATE"
)
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key), [self.default_digest])
with self.assertRaises(BadSignature):
ca.revokeCertificate(wrapped)
# payload with Fake certificate
fcert, fkey = self.get_fake_cert_key()
payload = dict(
reason="",
revoke_crt=cert
)
wrapped = utils.wrap(payload, fkey, [self.default_digest])
with self.assertRaises(BadSignature):
ca.revokeCertificate(wrapped)
payload = dict(
reason="",
revoke_crt=fcert
)
wrapped = utils.wrap(payload, fkey, [self.default_digest])
with self.assertRaises(CertificateVerificationError):
ca.revokeCertificate(wrapped)
def test_getCertificateRevocationList(self):
ca = self.make_ca(158)
def signcert():
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
return x509, key
cert_1, key_1 = signcert()
cert_2, key_2 = signcert()
cert2_string = crypto.dump_certificate(crypto.FILETYPE_PEM, cert_2)
cert_3, key_3 = signcert()
cert3_string = crypto.dump_certificate(crypto.FILETYPE_PEM, cert_3)
cert_4, key_4 = signcert()
crl = ca.getCertificateRevocationList()
crl_obj = crypto.load_crl(crypto.FILETYPE_PEM, crl)
self.assertEquals(crl_obj.get_revoked(), None)
payload = dict(
reason="",
revoke_crt=cert2_string
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key_2), [self.default_digest])
ca.revokeCertificate(wrapped)
crl2_string = ca.getCertificateRevocationList()
crl2 = crypto.load_crl(crypto.FILETYPE_PEM, crl2_string)
self.assertEquals(len(crl2.get_revoked()), 1)
serial = utils.getSerialToInt(cert_2)
self.assertEquals(crl2.get_revoked()[0].get_serial(), serial.upper())
payload = dict(
reason="",
revoke_crt=cert3_string
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key_3), [self.default_digest])
ca.revokeCertificate(wrapped)
crl3_string = ca.getCertificateRevocationList()
crl3 = crypto.load_crl(crypto.FILETYPE_PEM, crl3_string)
self.assertEquals(len(crl3.get_revoked()), 2)
matches = 0
for revoked in crl3.get_revoked():
if revoked.get_serial() == utils.getSerialToInt(cert_3).upper():
matches += 1
elif revoked.get_serial() == utils.getSerialToInt(cert_2).upper():
matches += 1
self.assertEquals(matches, 2)
crl4_string = ca.getCertificateRevocationList()
# nothing changed, crl not expired
self.assertEquals(crl3_string, crl4_string)
def test_getCertificateRevocationList_with_expire(self):
ca = self.make_ca(2)
def signcert():
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
return x509, key
cert_1, key_1 = signcert()
cert_2, key_2 = signcert()
cert2_string = crypto.dump_certificate(crypto.FILETYPE_PEM, cert_2)
crl_string = ca.getCertificateRevocationList()
crl = crypto.load_crl(crypto.FILETYPE_PEM, crl_string)
self.assertEquals(crl.get_revoked(), None)
payload = dict(
reason="",
revoke_crt=cert2_string
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key_2), [self.default_digest])
ca.revokeCertificate(wrapped)
crl2_string = ca.getCertificateRevocationList()
crl2 = crypto.load_crl(crypto.FILETYPE_PEM, crl2_string)
self.assertEquals(len(crl2.get_revoked()), 1)
serial = utils.getSerialToInt(cert_2)
self.assertEquals(crl2.get_revoked()[0].get_serial(), serial.upper())
# wait until cert_2 expire
time.sleep(3)
cert_3, key_3 = signcert()
cert3_string = crypto.dump_certificate(crypto.FILETYPE_PEM, cert_3)
payload = dict(
reason="",
revoke_crt=cert3_string
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key_3), [self.default_digest])
ca.revokeCertificate(wrapped)
crl3_string = ca.getCertificateRevocationList()
crl3 = crypto.load_crl(crypto.FILETYPE_PEM, crl3_string)
# cert_2 is not longer into crl (expired)
self.assertEquals(len(crl3.get_revoked()), 1)
serial = utils.getSerialToInt(cert_3)
self.assertEquals(crl3.get_revoked()[0].get_serial(), serial.upper())
def test_getCertificateRevocationList_with_validation(self):
ca = self.make_ca(158)
def signcert():
csr, key = self.generateCSR()
csr_id = ca.createCertificateSigningRequest(self.csr_tostring(csr))
# sign certificate with default ca keypair
cert_id = ca.createCertificate(csr_id)
cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key))
return x509, key
cert_1, key_1 = signcert()
cert_2, key_2 = signcert()
cert2_string = crypto.dump_certificate(crypto.FILETYPE_PEM, cert_2)
cert_3, key_3 = signcert()
payload = dict(
reason="",
revoke_crt=cert2_string
)
# sign with bad key
wrapped = utils.wrap(payload, crypto.dump_privatekey(crypto.FILETYPE_PEM, key_2), [self.default_digest])
ca.revokeCertificate(wrapped)
crl_string = ca.getCertificateRevocationList()
crl = crypto.load_crl(crypto.FILETYPE_PEM, crl_string)
self.assertEquals(len(crl.get_revoked()), 1)
serial = utils.getSerialToInt(cert_2)
self.assertEquals(crl.get_revoked()[0].get_serial(), serial.upper())
with self.assertRaises(CertificateVerificationError):
utils.verifyCertificateChain(cert_2,
[x['crt'] for x in ca._ca_key_pairs_list], crl)
utils.verifyCertificateChain(cert_3,
[x['crt'] for x in ca._ca_key_pairs_list], crl)
utils.verifyCertificateChain(cert_1,
[x['crt'] for x in ca._ca_key_pairs_list], crl)
# 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 os, time
import shutil
import tempfile
import unittest
import json
from datetime import datetime, timedelta
from caucase import app, db
from caucase.storage import Storage, Config
from OpenSSL import crypto, SSL
from caucase.exceptions import (NoStorage, NotFound, Found)
from sqlite3 import IntegrityError
from caucase import utils
import uuid
class StorageTest(unittest.TestCase):
def setUp(self):
self.ca_dir = tempfile.mkdtemp()
self.db_file = os.path.join(self.ca_dir, 'ca.db')
#os.mkdir(self.ca_dir)
self.max_request_amount = 3
self.default_digest = "sha256"
app.config.update(
DEBUG=True,
CSRF_ENABLED=True,
SECRET_KEY = 'This is an UNSECURE Secret. Please CHANGE THIS for production environments.',
TESTING=True,
SQLALCHEMY_DATABASE_URI='sqlite:///%s' % self.db_file
)
self._storage = Storage(db)
def setConfig(self, key, value):
entry = self._storage._getConfig(key)
if not entry:
entry = Config(key=key, value='%s' % value)
db.session.add(entry)
else:
# update value
entry.value = value
db.session.commit()
def tearDown(self):
db.session.remove()
db.drop_all()
if os.path.exists(self.ca_dir):
shutil.rmtree(self.ca_dir)
def createCAKeyPair(self, life_time=60):
key_pair = {}
key = crypto.PKey()
# Use 2048 bits key size
key.generate_key(crypto.TYPE_RSA, 2048)
key_pair['key'] = key
ca = crypto.X509()
ca.set_version(3)
ca.set_serial_number(int(time.time()))
ca.get_subject().CN = "CA Cert %s" % int(time.time())
ca.get_subject().C = "FR"
ca.get_subject().ST = "XX"
ca.get_subject().O = "ORG"
ca.get_subject().OU = "OU"
ca.get_subject().L = "LL"
ca.get_subject().emailAddress = "xxx@exemple.com"
ca.gmtime_adj_notBefore(0)
ca.gmtime_adj_notAfter(life_time)
ca.set_issuer(ca.get_subject())
ca.set_pubkey(key)
utils.X509Extension().setCaExtensions(ca)
ca.sign(key, self.default_digest)
key_pair['crt'] = ca
return key_pair
def generateCSR(self, cn="toto.example.com"):
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
req = crypto.X509Req()
subject = req.get_subject()
subject.CN = cn
subject.C = "CC"
subject.ST = "ST"
subject.L = "LL"
subject.O = "ORG"
subject.OU = "OU"
subject.emailAddress = "toto@example.com"
req.set_pubkey(key)
utils.X509Extension().setDefaultCsrExtensions(req)
req.sign(key, self.default_digest)
return (req, key)
def createCertificate(self, ca_key_pair, req, expire_sec=180):
serial = uuid.uuid1().int
cert = crypto.X509()
# 3 = v3
cert.set_version(3)
cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(expire_sec)
cert.set_issuer(ca_key_pair['crt'].get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
utils.X509Extension().setDefaultExtensions(
cert,
subject=cert,
issuer=ca_key_pair['crt'],
crl_url="http://ca.crl.com")
cert.sign(ca_key_pair['key'], self.default_digest)
return cert
def generateCRL(self, ca_key_pair, revocation_list, version_number):
now = datetime.now()
crl = crypto.CRL()
num_crl_days = 1
for revocation in revocation_list:
revoked = crypto.Revoked()
revoked.set_rev_date(
revocation.creation_date.strftime("%Y%m%d%H%M%SZ").encode("ascii")
)
revoked.set_serial(revocation.serial.encode("ascii"))
revoked.set_reason(None) #b'%s' % revocation.reason)
crl.add_revoked(revoked)
crl.set_version(version_number)
# XXX - set how to get the cacert here
cert = ca_key_pair['crt']
key = ca_key_pair['key']
return crypto.load_crl(
crypto.FILETYPE_PEM,
crl.export(
cert,
key,
type=crypto.FILETYPE_PEM,
days=num_crl_days,
digest=self.default_digest)
)
def check_cert_equal(self, first, second):
if isinstance(first, crypto.X509):
first_string = crypto.dump_certificate(crypto.FILETYPE_PEM, first)
else:
first_string = first
second_string = crypto.dump_certificate(crypto.FILETYPE_PEM, second)
return first_string == second_string
def check_key_equal(self, first, second):
if isinstance(first, crypto.PKey):
first_string = crypto.dump_privatekey(crypto.FILETYPE_PEM, first)
else:
first_string = first
second_string = crypto.dump_privatekey(crypto.FILETYPE_PEM, second)
return first_string == second_string
def check_csr_equal(self, first, second):
if isinstance(first, crypto.X509Req):
first_string = crypto.dump_certificate_request(crypto.FILETYPE_PEM, first)
else:
first_string = first
second_string = crypto.dump_certificate_request(crypto.FILETYPE_PEM, second)
return first_string == second_string
def test_db_exists(self):
self.assertTrue(os.path.exists(self.db_file))
def test_config(self):
# no config exists
self.assertEquals(self._storage.getConfig('config-sample'), None)
# with default value
self.assertEquals(self._storage.getConfig('config-sample', "tototo"), "tototo")
# set config (stored as string)
self.setConfig('config-test', 1458)
self.setConfig('test-dict', dict(first=23, second="sec"))
# values are string
self.assertEquals(self._storage.getConfig('config-test'), "1458")
self.assertEquals(self._storage.getConfig('test-dict'), str(dict(first=23, second="sec")))
def test_store_CAKeypair(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
# check stored keypair
stored = self._storage.getCAKeyPairList()
self.assertEquals(len(stored), 1)
self.assertTrue(self.check_cert_equal(stored[0]['crt'], keypair['crt']))
self.assertTrue(self.check_key_equal(stored[0]['key'], keypair['key']))
def test_store_same_keypair(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
# check stored keypair
stored = self._storage.getCAKeyPairList()
self.assertEquals(len(stored), 1)
self.assertTrue(self.check_cert_equal(stored[0]['crt'], keypair['crt']))
self.assertTrue(self.check_key_equal(stored[0]['key'], keypair['key']))
# store again the same keypair
with self.assertRaises(Found):
self._storage.storeCAKeyPair(keypair)
def test_store_keypair_multiple(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
# check stored keypair
stored = self._storage.getCAKeyPairList()
self.assertEquals(len(stored), 1)
self.assertTrue(self.check_cert_equal(stored[0]['crt'], keypair['crt']))
self.assertTrue(self.check_key_equal(stored[0]['key'], keypair['key']))
time.sleep(1)
keypair2 = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair2)
stored2 = self._storage.getCAKeyPairList()
self.assertEquals(len(stored2), 2)
# check that order of keypair is good
first = stored2[0]
self.assertTrue(self.check_cert_equal(first['crt'], keypair['crt']))
self.assertTrue(self.check_key_equal(first['key'], keypair['key']))
second = stored2[1]
self.assertTrue(self.check_cert_equal(second['crt'], keypair2['crt']))
self.assertTrue(self.check_key_equal(second['key'], keypair2['key']))
def test_storeCertificateSigningRequest(self):
csr, key = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
stored = self._storage.getPendingCertificateRequest(csr_id)
self.assertTrue(self.check_csr_equal(stored, csr))
def test_deletePendingCertificateRequest(self):
csr, key = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
stored = self._storage.getPendingCertificateRequest(csr_id)
self.assertTrue(self.check_csr_equal(stored, csr))
self._storage.deletePendingCertificateRequest(csr_id)
with self.assertRaises(NotFound):
self._storage.getPendingCertificateRequest(csr_id)
def test_storeCertificateSigningRequest_no_storage(self):
self.setConfig('max-csr-amount', self.max_request_amount)
self.assertEquals(int(self._storage.getConfig('max-csr-amount')), self.max_request_amount)
for i in range(0, self.max_request_amount):
csr, _ = self.generateCSR()
self._storage.storeCertificateSigningRequest(csr)
# will raise NoStorage now
csr, _ = self.generateCSR()
with self.assertRaises(NoStorage):
self._storage.storeCertificateSigningRequest(csr)
csr_list = self._storage.getPendingCertificateRequestList()
self.assertEquals(len(csr_list), self.max_request_amount)
def store_storeCertificateSigningRequest_same(self):
csr, key = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
stored = self._storage.getPendingCertificateRequest(csr_id)
self.assertTrue(self.check_csr_equal(stored, csr))
# store a second time the same csr
csr2_id = self._storage.storeCertificateSigningRequest(csr)
self.assertEquals(csr2_id, csr_id)
stored2 = self._storage.getPendingCertificateRequest(csr2_id)
self.assertEquals(stored2, stored)
csr_list = self._storage.getPendingCertificateRequestList()
# there is only on csr in the list
self.assertEquals(len(csr_list), 1)
def test_storeCertificate(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr)
cert_id = self._storage.storeCertificate(csr_id, cert)
stored = self._storage.getCertificate(cert_id)
self.assertTrue(self.check_cert_equal(stored, cert))
def test_storeCertificate_wrong_csr(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
csr, _ = self.generateCSR()
cert = self.createCertificate(keypair, csr)
csr_id="1234"
# csr_id not exists
with self.assertRaises(NotFound):
self._storage.storeCertificate(csr_id, cert)
csr_id = self._storage.storeCertificateSigningRequest(csr)
self._storage.deletePendingCertificateRequest(csr_id)
# csr was deleted
with self.assertRaises(NotFound):
self._storage.storeCertificate(csr_id, cert)
def test_storeCertificate_same(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr)
cert_id = self._storage.storeCertificate(csr_id, cert)
stored = self._storage.getCertificate(cert_id)
self.assertTrue(self.check_cert_equal(stored, cert))
with self.assertRaises(NotFound):
# CSR not found
self._storage.storeCertificate(csr_id, cert)
def test_storeCertificate_same_csr(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr)
cert_id = self._storage.storeCertificate(csr_id, cert)
stored = self._storage.getCertificate(cert_id)
self.assertTrue(self.check_cert_equal(stored, cert))
with self.assertRaises(NotFound):
# CSR not found
self._storage.storeCertificate(csr_id, cert)
csr2_id = self._storage.storeCertificateSigningRequest(csr)
self.assertNotEquals(csr2_id, csr_id)
#with self.assertRaises(IntegrityError):
# cannot store 2 certificate with same serial
# self._storage.storeCertificate(csr2_id, cert)
# can store new certificate, using a different csr2_id from csr
cert2 = self.createCertificate(keypair, csr)
cert2_id = self._storage.storeCertificate(csr2_id, cert2)
self.assertNotEquals(cert2_id, cert_id)
def test_revokeCertificate(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr)
cert_id = self._storage.storeCertificate(csr_id, cert)
expiration_date = datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ')
self._storage.revokeCertificate(utils.getSerialToInt(cert), expiration_date)
with self.assertRaises(NotFound):
# certificate was revoked
self._storage.getCertificate(cert_id)
revocation_list = self._storage.getRevocationList()
self.assertEquals(len(revocation_list), 1)
self.assertEquals(revocation_list[0].serial, utils.getSerialToInt(cert))
self.assertEquals(revocation_list[0].crt_expire_after, expiration_date)
def test_revokeCertificate_not_exists(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr)
cert_id = self._storage.storeCertificate(csr_id, cert)
expiration_date = datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ')
revocation_list = self._storage.getRevocationList()
self.assertEquals(revocation_list, [])
self._storage.revokeCertificate(utils.getSerialToInt(cert), expiration_date)
with self.assertRaises(NotFound):
# certificate was revoked
self._storage.revokeCertificate(utils.getSerialToInt(cert), expiration_date)
def test_storeCertificateRevocation_with_expired(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
def store_cert(expire_sec):
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr, expire_sec=expire_sec)
cert_id = self._storage.storeCertificate(csr_id, cert)
return cert_id, cert
id1, cert1 = store_cert(5)
id2, cert2 = store_cert(260)
id3, cert3 = store_cert(180)
expiration_date1 = datetime.strptime(cert1.get_notAfter(), '%Y%m%d%H%M%SZ')
expiration_date3 = datetime.strptime(cert3.get_notAfter(), '%Y%m%d%H%M%SZ')
self._storage.revokeCertificate(utils.getSerialToInt(cert1), expiration_date1)
self._storage.revokeCertificate(utils.getSerialToInt(cert3), expiration_date3)
revocation_list = self._storage.getRevocationList()
self.assertEquals(len(revocation_list), 2)
stored_cert = self._storage.getCertificate(id2)
self.assertTrue(self.check_cert_equal(stored_cert, cert2))
for i in [0, 1]:
if revocation_list[0].serial == utils.getSerialToInt(cert1):
self.assertEquals(revocation_list[0].crt_expire_after, expiration_date1)
revocation_list.pop(0)
elif revocation_list[0].serial == utils.getSerialToInt(cert3):
self.assertEquals(revocation_list[0].crt_expire_after, expiration_date3)
revocation_list.pop(0)
# All items was removed with pop(0)
self.assertEquals(revocation_list, [])
time.sleep(5)
revocation_list = self._storage.getRevocationList()
# revoked cert1 certificate has expired notAfter
self.assertEquals(len(revocation_list), 1)
self.assertEquals(revocation_list[0].serial, utils.getSerialToInt(cert3))
with self.assertRaises(NotFound):
self._storage.getCertificate(id1)
def test_getNextCRLVersionNumber(self):
serial = self._storage.getNextCRLVersionNumber()
self.assertEquals(serial, 1)
def test_storeCertificateRevocationList(self):
keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair)
def store_cert(expire_sec):
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr, expire_sec=expire_sec)
cert_id = self._storage.storeCertificate(csr_id, cert)
return cert_id, cert
id1, cert1 = store_cert(60)
id2, cert2 = store_cert(260)
id3, cert3 = store_cert(180)
expiration_date1 = datetime.strptime(cert1.get_notAfter(), '%Y%m%d%H%M%SZ')
expiration_date3 = datetime.strptime(cert3.get_notAfter(), '%Y%m%d%H%M%SZ')
self._storage.revokeCertificate(utils.getSerialToInt(cert1), expiration_date1)
self._storage.revokeCertificate(utils.getSerialToInt(cert3), expiration_date3)
revocation_list = self._storage.getRevocationList()
self.assertEquals(len(revocation_list), 2)
crl = self.generateCRL(keypair, revocation_list, 1)
self._storage.storeCertificateRevocationList(
crl,
expiration_date=(datetime.utcnow() + timedelta(0, 160))
)
stored = self._storage.getCertificateRevocationList()
self.assertNotEqual(stored, None)
crl_string = crypto.dump_crl(crypto.FILETYPE_PEM, crl)
self.assertEquals(crl_string, stored)
def test_getCertificateRevocationList_empty(self):
keypair = self.createCAKeyPair()
crl = self._storage.getCertificateRevocationList()
self.assertEqual(crl, None)
crl = self.generateCRL(keypair, [], 1)
self._storage.storeCertificateRevocationList(
crl,
expiration_date=(datetime.utcnow() + timedelta(0, 3))
)
stored = self._storage.getCertificateRevocationList()
self.assertNotEqual(stored, None)
crl_string = crypto.dump_crl(crypto.FILETYPE_PEM, crl)
self.assertEquals(crl_string, stored)
time.sleep(4)
stored = self._storage.getCertificateRevocationList()
# crl is not valid anymore
self.assertEqual(stored, None)
def test_housekeep(self):
from caucase.storage import CertificateRequest, Certificate, CAKeypair, CertificateRevocationList, Revocation
self.setConfig('crt-keep-time', 5)
self.setConfig('csr-keep-time', 5)
# ca cert expire after 4 seconds
keypair = self.createCAKeyPair(5)
self._storage.storeCAKeyPair(keypair)
time.sleep(1)
keypair2 = self.createCAKeyPair(15)
self._storage.storeCAKeyPair(keypair2)
self.assertEquals(len(self._storage.getCAKeyPairList()), 2)
def store_cert(expire_sec):
csr, _ = self.generateCSR()
csr_id = self._storage.storeCertificateSigningRequest(csr)
cert = self.createCertificate(keypair, csr, expire_sec=expire_sec)
cert_id = self._storage.storeCertificate(csr_id, cert)
return cert_id, cert
id1, cert1 = store_cert(4)
id2, cert2 = store_cert(4)
id3, cert3 = store_cert(8)
expiration_date1 = datetime.strptime(cert2.get_notAfter(), '%Y%m%d%H%M%SZ')
self._storage.revokeCertificate(utils.getSerialToInt(cert2), expiration_date1)
revocation_list = self._storage.getRevocationList()
self.assertEquals(len(revocation_list), 1)
crl = self.generateCRL(keypair, revocation_list, 2)
self._storage.storeCertificateRevocationList(
crl,
expiration_date=(datetime.utcnow() + timedelta(0, 4))
)
time.sleep(2)
id_4, cert4 = store_cert(10)
id_5, cert5 = store_cert(10)
expiration_date3 = datetime.strptime(cert4.get_notAfter(), '%Y%m%d%H%M%SZ')
self._storage.revokeCertificate(utils.getSerialToInt(cert4), expiration_date3)
revocation_list = self._storage.getRevocationList()
self.assertEquals(len(revocation_list), 2)
crl2 = self.generateCRL(keypair, revocation_list, 3)
self._storage.storeCertificateRevocationList(
crl,
expiration_date=(datetime.utcnow() + timedelta(0, 10))
)
time.sleep(3)
crl_list = CertificateRevocationList.query.filter().all()
self.assertEquals(len(crl_list), 2)
revocation_list = Revocation.query.filter().all()
self.assertEquals(len(revocation_list), 2)
self._storage.housekeep()
# One key_pair is expired
stored = self._storage.getCAKeyPairList()
self.assertEquals(len(stored), 1)
self.assertTrue(self.check_cert_equal(stored[0]['crt'], keypair2['crt']))
self.assertTrue(self.check_key_equal(stored[0]['key'], keypair2['key']))
csr_count = CertificateRequest.query.filter().count()
# 3 csr should be removed
self.assertEquals(csr_count, 2)
with self.assertRaises(NotFound):
self._storage.getCertificate(id1)
self._storage.getCertificate(id2)
self._storage.getCertificate(id3)
with self.assertRaises(NotFound):
# this certificate is revoked
self._storage.getCertificate(id_4)
# content still exists (keep-time not expired)
self._storage.getCertificate(id_5)
# certificate informations are not remove
cert_count = Certificate.query.filter().count()
self.assertEquals(cert_count, 5)
revocation_list = Revocation.query.filter().all()
self.assertEquals(len(revocation_list), 1)
self.assertTrue(revocation_list[0].serial == utils.getSerialToInt(cert4))
# one crl is removed
crl_list = CertificateRevocationList.query.filter().all()
self.assertEquals(len(crl_list), 1)
# 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 os, time
import urllib2
import shutil
import tempfile
import json
from datetime import datetime
from caucase.web import parseArguments, configure_flask
from OpenSSL import crypto, SSL
from caucase.exceptions import (NoStorage, NotFound, Found)
from caucase import utils
from flask_testing import TestCase
from flask import url_for
class CertificateAuthorityWebTest(TestCase):
def setUp(self):
self.ca_dir = tempfile.mkdtemp()
configure_flask(parseArguments(['--ca-dir', self.ca_dir, '-s', '/CN=CA Auth Test/emailAddress=xx@example.com']))
def tearDown(self):
self.db.session.remove()
self.db.drop_all()
if os.path.exists(self.ca_dir):
shutil.rmtree(self.ca_dir)
def create_app(self):
from caucase import db, app
app.config['TESTING'] = True
app.config['LIVESERVER_PORT'] = 0
self.db = db
return app
def generateCSR(self, cn="toto.example.com"):
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
req = crypto.X509Req()
subject = req.get_subject()
subject.CN = cn
subject.C = "CC"
subject.ST = "ST"
subject.L = "LO"
subject.O = "ORG"
subject.OU = "OU"
subject.emailAddress = "toto@example.com"
req.set_pubkey(key)
utils.X509Extension().setDefaultCsrExtensions(req)
req.sign(key, 'sha256')
return (req, key)
def _init_password(self, password):
response = self.client.post('/admin/setpassword', data=dict(password=password),
follow_redirects=True)
self.assert200(response)
def test_get_crl(self):
response = self.client.get('/crl')
self.assert200(response)
crypto.load_crl(crypto.FILETYPE_PEM, response.data)
def test_put_csr(self):
csr, _ = self.generateCSR()
csr_string = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
data = dict(csr=csr_string)
response = self.client.put(url_for('request_cert'), data=data)
self.assertEquals(response.status_code, 201)
csr_key = response.headers['Location'].split('/')[-1]
# the first csr is signed and csr is no longer available
response2 = self.client.get('/csr/%s' % csr_key)
self.assert404(response2)
cert_url = '/crt/%s.crt.pem' % csr_key[:-8]
response3 = self.client.get(cert_url)
self.assert200(response3)
crypto.load_certificate(crypto.FILETYPE_PEM, response3.data)
# put another csr
csr2, _ = self.generateCSR()
csr2_string = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
response4 = self.client.put(url_for('request_cert'), data=dict(csr=csr2_string))
self.assertEquals(response4.status_code, 201)
csr_key = response4.headers['Location'].split('/')[-1]
# get csr will return the csr
response5 = self.client.get('/csr/%s' % csr_key)
self.assert200(response5)
# the certificate is not signed
cert_url = '/crt/%s.crt.pem' % csr_key[:-8]
response6 = self.client.get(cert_url)
self.assert404(response6)
self.assertEquals(response5.data, csr2_string)
def test_get_csr_notfound(self):
response = self.client.get('/csr/1234')
self.assert404(response)
# self.assertEquals(response.json['name'], 'FileNotFound'))
def test_get_cacert(self):
response = self.client.get('/crt/ca.crt.pem')
self.assert200(response)
crypto.load_certificate(crypto.FILETYPE_PEM, response.data)
......@@ -15,240 +15,396 @@
#
# You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
Small-ish functions needed in many places.
"""
from __future__ import absolute_import
from collections import defaultdict
import datetime
import json
import os
import time
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding
import cryptography.exceptions
import pem
from .exceptions import CertificateVerificationError
DEFAULT_DIGEST_LIST = ('sha256', 'sha384', 'sha512')
DEFAULT_DIGEST = DEFAULT_DIGEST_LIST[0]
DEFAULT_DIGEST_CLASS = getattr(hashes, DEFAULT_DIGEST.upper())
# load-time sanity check
def _checkDefaultDigestsAvailable():
for x in DEFAULT_DIGEST_LIST:
getattr(hashes, x.upper())
_checkDefaultDigestsAvailable()
del _checkDefaultDigestsAvailable
_cryptography_backend = default_backend()
# Registration-less OID under 2.25 tree (aka uuid tree)
CAUCASE_OID_TOP = '2.25.285541874270823339875695650038637483517'
CAUCASE_OID_AUTO_SIGNED = CAUCASE_OID_TOP + '.0'
# Reserved for tests: no meaning, always stripped but never specificaly
# checked for in the code.
CAUCASE_OID_RESERVED = CAUCASE_OID_TOP + '.999'
_CAUCASE_OID_AUTO_SIGNED = x509.oid.ObjectIdentifier(CAUCASE_OID_AUTO_SIGNED)
CAUCASE_POLICY_INFORMATION_AUTO_SIGNED = x509.PolicyInformation(
_CAUCASE_OID_AUTO_SIGNED,
[
x509.UserNotice(
None,
'Auto-signed caucase certificate',
),
]
)
def isCertificateAutoSigned(crt):
"""
Checks whether given certificate was automatically signed by caucase.
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
Allows ensuring no rogue certificate could be emitted before legitimate owner
could take control of their instance: in such case, "first" certificate would
not appear as auto-signed.
class GeneralNames(rfc2459.GeneralNames):
"""
rfc2459 has wrong tagset.
Returns True if certificate is auto-signed, False otherwise.
"""
tagSet = tag.TagSet(
(),
tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0),
try:
extension = crt.extensions.get_extension_for_class(
x509.CertificatePolicies,
)
except x509.ExtensionNotFound:
pass
else:
for policy_information in extension.value:
if policy_information.policy_identifier == _CAUCASE_OID_AUTO_SIGNED:
return True
return False
class DistributionPointName(rfc2459.DistributionPointName):
"""
rfc2459 has wrong tagset.
"""
tagSet = tag.TagSet(
(),
tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0),
def _getPEMTypeDict(path, result=None):
if result is None:
result = defaultdict(list)
for entry in pem.parse_file(path):
result[pem.Key if isinstance(entry, pem.Key) else type(entry)].append(
entry,
)
return result
class ExtensionType():
def getCertList(crt_path):
"""
Return a list of certificates.
Raises if there is anything else than a certificate.
"""
type_dict = _getPEMTypeDict(crt_path)
crt_list = type_dict.pop(pem.Certificate)
if type_dict:
raise ValueError('%s contains more than just certificates' % (crt_path, ))
return [x.as_bytes() for x in crt_list]
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"
def getCert(crt_path):
"""
Return a certificate from a file which may also contain a key.
Raises if there is more or less than one certificate.
"""
type_dict = _getPEMTypeDict(crt_path)
crt, = type_dict.get(pem.Certificate)
return crt.as_bytes()
def hasOneCert(crt_path):
"""
Returns whether crt_path contains a certificate.
class X509Extension(object):
False if there is no file at crt_path.
Raises if there is more than one certificate.
Ignores other types.
"""
if os.path.exists(crt_path):
crt_list = _getPEMTypeDict(crt_path).get(pem.Certificate, [])
if crt_list:
_, = crt_list
return True
return False
known_extension_list = [name for (attr, name) in vars(ExtensionType).items()
if attr.isupper()]
def getCertRequest(csr_path):
"""
Return a certificate request from a file.
Raises if there is more or less than one certificate request, or anything
else.
"""
type_dict = _getPEMTypeDict(csr_path)
csr, = type_dict.pop(pem.CertificateRequest)
if type_dict:
raise ValueError('%s contains more than just a csr' % (csr_path, ))
return csr.as_bytes()
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')
def getKey(key_path):
"""
Return a key from a file.
Raises if there is more or less than one key, or anything else.
"""
type_dict = _getPEMTypeDict(key_path)
key, = type_dict.pop(pem.Key)
if type_dict:
raise ValueError('%s contains more than just a key' % (key_path, ))
return key.as_bytes()
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,
)
def getKeyPair(crt_path, key_path=None):
"""
Return a certificate and a key from a pair of file.
If crt_path contains both a cert and a key, key_path is ignored.
Raises if there is more than one certificate or more than one key.
Raises if key and cert do not match.
"""
type_dict = _getPEMTypeDict(crt_path)
if pem.Key not in type_dict and key_path:
_getPEMTypeDict(key_path, type_dict)
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()))
key_path = None
key, = type_dict[pem.Key]
crt, = type_dict[pem.Certificate]
key = key.as_bytes()
crt = crt.as_bytes()
validateCertAndKey(crt, key)
return crt, key, key_path
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
"""
Verify certificate and key match.
def verifyCertificateChain(cert_pem, trusted_cert_list, crl=None):
Raises if it is not the case.
"""
if x509.load_pem_x509_certificate(
cert_pem,
_cryptography_backend,
).public_key().public_numbers() != load_privatekey(
key_pem,
).public_key().public_numbers():
raise ValueError('Mismatch between private key and certificate')
def _verifyCertificateChain(cert, trusted_cert_list, crl):
"""
Verifies whether certificate has been signed by any of the trusted
certificates, is not revoked and is whithin its validity period.
# Create and fill a X509Sore with trusted certs
Raises CertificateVerificationError if validation fails.
"""
# Note: this function (validating a certificate without an SSL connection)
# does not seem to have many equivalents at all in python. OpenSSL module
# seems to be a rare implementation of it, so we keep using this module.
# BUT it MUST NOT be used anywhere outside this function (hence the
# bad-style local import). Use "cryptography".
from OpenSSL import crypto
store = crypto.X509Store()
assert trusted_cert_list
for trusted_cert in trusted_cert_list:
store.add_cert(trusted_cert)
if crl:
store.add_crl(crl)
store.add_cert(crypto.X509.from_cryptography(trusted_cert))
if crl is not None:
store.add_crl(crypto.CRL.from_cryptography(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:
crypto.X509StoreContext(
store,
crypto.X509.from_cryptography(cert),
).verify_certificate()
except (
crypto.X509StoreContextError,
crypto.Error,
), e:
raise CertificateVerificationError('Certificate verification error: %s' % str(e))
if result is None:
return True
else:
return False
def wrap(payload, key, digest):
"""
Sign payload (which gets json-serialised) with key, using given digest.
"""
payload = json.dumps(payload).encode('utf-8')
hash_class = getattr(hashes, digest.upper())
return {
'payload': payload,
'digest': digest,
'signature': key.sign(
payload + digest + ' ',
padding.PSS(
mgf=padding.MGF1(hash_class()),
salt_length=padding.PSS.MAX_LENGTH,
),
hash_class(),
).encode('base64'),
}
def checkCertificateValidity(ca_cert_list, cert_pem, key_pem=None):
def nullWrap(payload):
"""
Wrap without signature. To only be used (and accepted) when user is
authenticated (and hence using a secure channel, HTTPS).
"""
return {
'payload': json.dumps(payload),
'digest': None,
}
if not verifyCertificateChain(cert_pem, ca_cert_list):
return False
if key_pem:
return validateCertAndKey(cert_pem, key_pem)
return True
def unwrap(wrapped, getCertificate, digest_list):
"""
Check payload signature and return it.
Raises cryptography.exceptions.InvalidSignature if signature does not match
payload or if transmitted digest is not an acceptable one.
def sign(data, key, digest="sha256"):
Note: does *not* verify received certificate itself (validity, issuer, ...).
"""
Sign a data using digest and return signature.
# Check whether given digest is allowed
digest = wrapped['digest'].encode('ascii')
if digest not in digest_list:
raise cryptography.exceptions.UnsupportedAlgorithm(
'%r is not in allowed digest list',
)
hash_class = getattr(hashes, digest.upper())
payload = json.loads(wrapped['payload'])
x509.load_pem_x509_certificate(
getCertificate(payload).encode('ascii'),
_cryptography_backend,
).public_key().verify(
wrapped['signature'].encode('ascii').decode('base64'),
wrapped['payload'].encode('utf-8') + digest + ' ',
padding.PSS(
mgf=padding.MGF1(hash_class()),
salt_length=padding.PSS.MAX_LENGTH,
),
hash_class(),
)
return payload
def nullUnwrap(wrapped):
"""
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
Un-wrapp unsigned content. To onl be used on content received from
an authenticated user (and hence over a secure channel, HTTPS).
"""
assert wrapped['digest'] is None
return json.loads(wrapped['payload'])
sign = crypto.sign(pkey, data, digest)
#data_base64 = base64.b64encode(sign)
def load_ca_certificate(data):
"""
Load CA certificate from PEM-encoded data.
return sign
Raises CertificateVerificationError if loaded certificate is not self-signed
or is otherwise invalid.
"""
crt = x509.load_pem_x509_certificate(data, _cryptography_backend)
_verifyCertificateChain(crt, [crt], None)
return crt
def verify(data, cert_string, signature, digest="sha256"):
def load_certificate(data, trusted_cert_list, crl):
"""
Verify the signature for a data string.
Load a certificate from PEM-encoded data.
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
Raises CertificateVerificationError if loaded certificate is not signed by
any of trusted certificates, is revoked or is otherwise invalid.
"""
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_string)
return crypto.verify(x509, signature, data, digest.encode("ascii", 'ignore'))
crt = x509.load_pem_x509_certificate(data, _cryptography_backend)
_verifyCertificateChain(crt, trusted_cert_list, crl)
return crt
def wrap(payload, key, digest_list):
def dump_certificate(data):
"""
Sign payload (json-serialised) with key, using one of the given digests.
Serialise a certificate as PEM-encoded data.
"""
# 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'),
}
return data.public_bytes(serialization.Encoding.PEM)
def unwrap(wrapped, getCertificate, digest_list):
def load_certificate_request(data):
"""
Raise if signature does not match payload. Returns payload.
Load a certificate request from PEM-encoded data.
Raises cryptography.exceptions.InvalidSignature if certificate signature
does not match embedded public key.
"""
result = x509.load_pem_x509_csr(data, _cryptography_backend)
if not result.is_signature_valid:
raise cryptography.exceptions.InvalidSignature
return result
def dump_certificate_request(data):
"""
Serialise acertificate request as PEM-encoded data.
"""
return data.public_bytes(
encoding=serialization.Encoding.PEM,
)
def load_privatekey(data):
"""
Load a private key from PEM-encoded data.
"""
return serialization.load_pem_private_key(
data,
password=None,
backend=_cryptography_backend,
)
def dump_privatekey(data):
"""
Serialise a private key as PEM-encoded data.
"""
return data.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
def generatePrivateKey(key_len):
"""
Generate a new private key of specified length.
"""
return rsa.generate_private_key(
public_exponent=65537,
key_size=key_len,
backend=_cryptography_backend,
)
def load_crl(data, trusted_cert_list):
"""
Load a certificate revocation list from PEM-encoded data.
If cryptography supports it, raises cryptography.exceptions.InvalidSignature
if the CRL signature does not match any trusted certificate.
"""
crl = x509.load_pem_x509_crl(data, _cryptography_backend)
for cert in trusted_cert_list:
# TODO: make mandatory when next cryptography version is released
if getattr(crl, 'is_signature_valid', lambda x: True)(cert.public_key()):
return crl
raise cryptography.exceptions.InvalidSignature
EPOCH = datetime.datetime(1970, 1, 1)
def datetime2timestamp(value):
"""
Convert given datetime into a unix timestamp.
"""
return (value - EPOCH).total_seconds()
class SleepInterrupt(KeyboardInterrupt):
"""
A sleep was interrupted by a KeyboardInterrupt
"""
pass
def interruptibleSleep(duration):
"""
Like sleep, but raises SleepInterrupt when interrupted by KeyboardInterrupt
"""
# 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
time.sleep(duration)
except KeyboardInterrupt:
raise SleepInterrupt
def until(deadline):
"""
Call interruptibleSleep until deadline is reached.
"""
now = datetime.datetime.utcnow()
while now < deadline:
interruptibleSleep((deadline - now).total_seconds())
now = datetime.datetime.utcnow()
return now
# 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 logging
import os, errno
import argparse
import traceback
import json
import flask
from flask import (session, request, redirect, url_for, render_template,
jsonify, abort, send_file, flash, g, Response)
from flask_user import UserManager, SQLAlchemyAdapter
from wtforms import StringField, SubmitField, validators
from flask_wtf import FlaskForm
from flask_mail import Mail
from flask_user import login_required, current_user
from flask_login import logout_user #, login_user, current_user, login_required
from caucase.ca import CertificateAuthority, DEFAULT_DIGEST_LIST, MIN_CA_RENEW_PERIOD
from caucase.exceptions import (NoStorage, NotFound, Found, BadSignature,
BadCertificateSigningRequest,
BadCertificate,
CertificateVerificationError,
ExpiredCertificate)
from functools import wraps
from caucase import utils, app, db
class DisabledStringField(StringField):
def __call__(self, *args, **kwargs):
kwargs.setdefault('disabled', True)
return super(ReadonlyStringField, self).__call__(*args, **kwargs)
# Define the User profile form
class UserProfileForm(FlaskForm):
username = StringField('Username')
first_name = StringField('First name', validators=[
validators.DataRequired('First name is required')])
last_name = StringField('Last name', validators=[
validators.DataRequired('Last name is required')])
email = StringField('Email', validators=[
validators.DataRequired('Email is required')])
submit = SubmitField('Save')
def parseArguments(argument_list=[]):
"""
Parse arguments for Certificate Authority instance.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--ca-dir',
help='Certificate authority base directory')
parser.add_argument('-H', '--host',
default='127.0.0.1',
help='Host or IP of ca server. Default: %(default)s')
parser.add_argument('-P', '--port',
default='9086', type=int,
help='Port for ca server. Default: %(default)s')
parser.add_argument('-d', '--debug',
action="store_true", dest="debug", default=False,
help='Enable debug mode.')
parser.add_argument('-l', '--log-file',
help='Path for log output')
parser.add_argument('--crt-life-time',
default=365*24*60*60, type=int,
help='The time in seconds before a generated certificate will expire. Default: 365*24*60*60 seconds (1 year)')
parser.add_argument('-s', '--subject',
default='',
help='Formatted subject string to put into generated CA Certificate file. Ex: /C=XX/ST=State/L=City/OU=OUnit/O=Company/CN=CAAuth/emailAddress=xx@example.com')
parser.add_argument('--ca-life-period',
default=10, type=float,
help='Number of individual certificate validity periods during which the CA certificate is valid. Default: %(default)s')
parser.add_argument('--ca-renew-period',
default=MIN_CA_RENEW_PERIOD, type=float,
help='Number of individual certificate validity periods during which both the existing and the new CA Certificates are valid. Default: %(default)s')
parser.add_argument('--crl-life-period',
default=1/50., type=float,
help='Number of individual certificate validity periods during which the CRL is valid. Default: %(default)s')
parser.add_argument('-D', '--digest',
action='append', dest='digest_list', default=DEFAULT_DIGEST_LIST,
help='Allowed digest for all signature. Default: %(default)s')
parser.add_argument('--max-request-amount',
default=50,
help='Maximun pending certificate signature request amount. Default: %(default)s')
parser.add_argument('--crt-keep-time',
default=30*24*60*60, type=int,
help='The time in seconds before a generated certificate will be deleted on CA server. Set 0 to never delete. Default: 30*24*60*60 seconds (30 days)')
parser.add_argument('--external-url',
help="The HTTP URL at which this tool's \"/\" path is reachable by all certificates users in order to retrieve latest CRL.")
parser.add_argument('--auto-sign-csr-amount',
default=1, type=int,
help='Say how many csr must be signed automatically. Has no effect if there is more than the specified value of csr submitted.')
if argument_list:
return parser.parse_args(argument_list)
return parser.parse_args()
def getLogger(debug=False, log_file=None):
logger = logging.getLogger("CertificateAuthority")
logger.setLevel(logging.INFO)
if not log_file:
logger.addHandler(logging.StreamHandler())
else:
handler = logging.FileHandler(log_file)
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(handler)
logger.info('Configured logging to file %r' % log_file)
if debug:
logger.setLevel(logging.DEBUG)
return logger
def getConfig(self, key):
if key in self.keys():
temp_dict = dict()
temp_dict.update(self)
return temp_dict[key]
else:
raise KeyError
def start():
"""
Start Web Flask application server
"""
options = parseArguments()
configure_flask(options)
app.logger.info("Certificate Authority server started on http://%s:%s" % (
options.host, options.port))
app.run(
host=options.host,
port=int(options.port)
)
def configure_flask(options):
"""
Configure certificate authority service
"""
if not options.ca_dir:
options.ca_dir = os.getcwd()
else:
options.ca_dir = os.path.abspath(options.ca_dir)
if not options.external_url:
options.external_url = 'http://[%s]:%s' % (options.host, options.port)
db_file = "sqlite:///%s"% os.path.join(options.ca_dir, 'ca.db')
# work in ca directory
os.chdir(options.ca_dir)
# init Flask app
app.config.update(
DEBUG=options.debug,
CSRF_ENABLED=True,
USER_AFTER_LOGIN_ENDPOINT='manage_csr',
USER_AFTER_LOGOUT_ENDPOINT='index',
USER_ENABLE_USERNAME=True,
USER_ENABLE_EMAIL=False,
USER_ENABLE_REGISTRATION=False,
USER_ENABLE_CHANGE_USERNAME=False,
SECRET_KEY = 'This is an UNSECURE Secret. Please CHANGE THIS for production environments.',
SQLALCHEMY_DATABASE_URI=db_file
)
flask.config.Config.__getattr__ = getConfig
mail = Mail(app)
# Setup Flask-User
# XXX - User table Will go away when switching to CA for Users
if not app.config['TESTING']:
from caucase.storage import User
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
user_manager = UserManager(db_adapter, app) # Initialize Flask-User
logger = getLogger(options.debug, options.log_file)
app.logger.addHandler(logger)
# Instanciate storage
from caucase.storage import Storage
storage = Storage(db,
max_csr_amount=options.max_request_amount,
crt_keep_time=options.crt_keep_time,
csr_keep_time=options.crt_keep_time)
ca = CertificateAuthority(
storage=storage,
ca_life_period=options.ca_life_period,
ca_renew_period=options.ca_renew_period,
crt_life_time=options.crt_life_time,
crl_renew_period=options.crl_life_period,
digest_list=options.digest_list,
crl_base_url='%s/crl' % options.external_url,
ca_subject=options.subject,
auto_sign_csr_amount=options.auto_sign_csr_amount
)
# XXX - Storage argument Will go away when switching to CA for Users
app.config.update(
storage=storage,
ca=ca,
log_file=options.log_file,
)
def check_authentication(username, password):
user = app.config.storage.findUser(username)
if user:
return app.user_manager.verify_password(password, user)
else:
return False
def authenticated_method(func):
""" This decorator ensures that the current user is logged in before calling the actual view.
Abort with 401 when the user is not logged in."""
@wraps(func)
def decorated_view(*args, **kwargs):
# User must be authenticated
auth = request.authorization
if not auth:
return abort(401)
elif not check_authentication(auth.username, auth.password):
return abort(401)
# Call the actual view
return func(*args, **kwargs)
return decorated_view
class FlaskException(Exception):
status_code = 400
code = 400
def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self)
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
# rv['code'] = self.code
rv['message'] = self.message
return rv
@app.errorhandler(FlaskException)
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
@app.errorhandler(401)
def error401(error):
if error.description is None:
message = {
'code': 401,
'name': 'Unauthorized',
'message': "Authenticate."
}
else:
message = error.description
response = jsonify(message)
response.status_code = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return response
@app.errorhandler(403)
def error403(error):
if error.description is None:
message = {
'code': 404,
'name': 'Forbidden',
'message': 'Forbidden. Your are not allowed to access %s' % request.url,
}
else:
message = error.description
response = jsonify(message)
response.status_code = 404
return response
@app.errorhandler(404)
def error404(error):
if error.description is None:
message = {
'code': 404,
'name': 'NotFound',
'message': 'Resource not found: ' + request.url,
}
else:
message = error.description
response = jsonify(message)
response.status_code = 404
return response
@app.errorhandler(400)
def error400(error):
if error.description is None:
message = {
'code': 400,
'name': 'BadRequest',
'message': 'The request could not be understood by the server, you probably provided wrong parameters.'
}
else:
message = error.description
response = jsonify(message)
response.status_code = 400
return response
def send_file_content(content, filename, mimetype='text/plain'):
return Response(content,
mimetype=mimetype,
headers={"Content-Disposition":
"attachment;filename=%s" % filename})
@app.before_request
def before_request():
# XXX - This function Will be modified or removed when switching to CA for Users
is_admin_path = request.path.startswith('/admin') or request.path.startswith('/user')
if not is_admin_path and not request.path.startswith('/certificates'):
return
if is_admin_path:
csr_count = app.config.storage.countPendingCertificateSiningRequest()
if csr_count < 10:
csr_count = '0%s' % csr_count
session['count_csr'] = csr_count
if request.path == '/admin/configure' or request.path == '/admin/setpassword':
# check if password file exists, if yes go to index
if app.config.storage.findUser('admin'):
return redirect(url_for('admin'))
return
# XXX - using hard username
if not app.config.storage.findUser('admin'):
return redirect(url_for('configure'))
g.user = current_user
# Routes for certificate Authority
@app.route('/crl', methods=['GET'])
def get_crl():
"""
Get the lastest CRL (certificate revocation list)
"""
crl_content = app.config.ca.getCertificateRevocationList()
return send_file_content(crl_content, 'ca.crl.pem')
@app.route('/csr/<string:csr_id>', methods=['GET'])
def get_csr(csr_id):
"""
Get a CSR string in PEM format from identified by `csr_id`.
"""
try:
csr_content = app.config.ca.getPendingCertificateRequest(csr_id)
except NotFound, e:
raise FlaskException(str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
return send_file_content(csr_content, csr_id)
@app.route('/csr', methods=['PUT'])
def request_cert():
"""
Store certificate signature request (csr) in PEM format
"""
csr_content = request.form.get('csr', '').encode('utf-8')
if not csr_content:
raise FlaskException("'csr' parameter is mandatory",
payload={"name": "MissingParameter", "code": 2})
try:
csr_id = app.config.ca.createCertificateSigningRequest(csr_content)
except BadCertificateSigningRequest, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except NoStorage, e:
raise FlaskException(str(e),
payload={"name": "NoStorage", "code": 4})
response = Response("", status=201)
response.headers['Location'] = url_for('get_csr', _external=True, csr_id=csr_id)
return response
@app.route('/csr/<string:csr_id>', methods=['DELETE'])
@authenticated_method
def remove_csr(csr_id):
"""
Delete a Certificate signature request. Authentication required
"""
try:
app.config.ca.deletePendingCertificateRequest(csr_id)
except NotFound, e:
raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
response = Response("", status=200)
return response
@app.route('/crt/<string:cert_id>', methods=['GET'])
def get_crt(cert_id):
"""
Get a certificate by the id `cert_id`
"""
try:
cert_content = app.config.ca.getCertificate(cert_id)
except NotFound, e:
raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
return send_file_content(cert_content, cert_id)
@app.route('/crt/serial/<string:serial>', methods=['GET'])
def crt_fromserial(serial):
"""
Get a certificate by the serial
"""
try:
cert_content = app.config.ca.getCertificateFromSerial(serial)
except NotFound, e:
raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
return send_file_content(cert_content, '%s.crt.pem' % serial)
@app.route('/crt/ca.crt.pem', methods=['GET'])
def get_cacert():
"""
Get CA Certificate in PEM format string.
"""
ca_cert = app.config.ca.getCACertificate()
return send_file_content(ca_cert, 'ca.crt.pem')
@app.route('/crt/ca.crt.json', methods=['GET'])
def get_cacert_json():
"""
Return CA certificate chain list, if the CA certificate is being renewed
the list will contain the next certificate and the old certificate which
will expire soon.
"""
ca_chain_list = app.config.ca.getValidCACertificateChain()
return jsonify(ca_chain_list)
def signcert(csr_key, subject_dict=None, redirect_to=''):
try:
cert_id = app.config.ca.createCertificate(csr_key, subject_dict=subject_dict)
except NotFound, e:
raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
except Found, e:
# Certificate is found
raise FlaskException("%s" % str(e),
payload={"name": "FileFound", "code": 5})
# XXX - to remove (flask UI)
flash('Certificate is signed!', 'success')
if redirect_to:
return redirect(url_for(redirect_to))
response = Response("", status=201)
response.headers['Location'] = url_for('get_crt', _external=True, cert_id=cert_id)
return response
@app.route('/crt', methods=['PUT'])
@authenticated_method
def sign_cert():
"""
Sign a certificate, require authentication
"""
key = request.form.get('csr_id', '').encode('utf-8')
if not key:
raise FlaskException("'csr_id' parameter is a mandatory parameter",
payload={"name": "MissingParameter", "code": 2})
try:
subject = request.form.get('subject', '').encode('utf-8')
subject_dict = None
if subject:
subject_dict = json.loads(subject)
return signcert(key, subject_dict=subject_dict)
except ValueError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except AttributeError, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
@app.route('/crt/renew', methods=['PUT'])
def renew_cert():
"""
this method is used to renew expired certificate.
"""
payload = request.form.get('payload', '')
if not payload:
# Bad parameters
raise FlaskException("'payload' parameter is mandatory",
payload={"name": "MissingParameter", "code": 2})
try:
wrapped = json.loads(payload)
except ValueError, e:
raise FlaskException("payload parameter is not a valid Json string: %s" % str(e),
payload={"name": "FileFormat", "code": 3})
try:
cert_id = app.config.ca.renew(wrapped)
except ValueError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except KeyError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except BadSignature, e:
raise FlaskException(str(e),
payload={"name": "SignatureMismatch", "code": 6})
except BadCertificateSigningRequest, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except BadCertificate, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except CertificateVerificationError, e:
raise FlaskException(str(e),
payload={"name": "SignatureMismatch", "code": 6})
except NoStorage, e:
raise FlaskException(str(e),
payload={"name": "NoStorage", "code": 4})
except NotFound, e:
raise FlaskException(str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
except Found, e:
# Certificate is found
raise FlaskException(str(e),
payload={"name": "FileFound", "code": 5})
response = Response("", status=201)
response.headers['Location'] = url_for('get_crt', _external=True, cert_id=cert_id)
return response
@app.route('/crt/revoke', methods=['PUT'])
def request_revoke_crt():
"""
Revoke method existing and valid certificate
"""
payload = request.form.get('payload', '')
if not payload:
# Bad parameters
raise FlaskException("'payload' parameter is mandatory",
payload={"name": "MissingParameter", "code": 2})
try:
wrapped = json.loads(payload)
except ValueError, e:
raise FlaskException("payload parameter is not a valid Json string: %s" % str(e),
payload={"name": "FileFormat", "code": 3})
try:
app.config.ca.revokeCertificate(wrapped)
except ValueError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except KeyError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except BadSignature, e:
raise FlaskException(str(e),
payload={"name": "SignatureMismatch", "code": 6})
except CertificateVerificationError, e:
raise FlaskException(str(e),
payload={"name": "SignatureMismatch", "code": 6})
except BadCertificate, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except ExpiredCertificate, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except NotFound, e:
raise FlaskException(str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
response = Response("", status=201, )
return response
@app.route('/crt/revoke/id', methods=['PUT'])
@authenticated_method
def revoke_crt():
"""
Directly revoke a certificate from his serial
"""
try:
crt_id = request.form.get('crt_id', '')
if not crt_id:
raise FlaskException("'crt_id' parameter is mandatory",
payload={"name": "MissingParameter", "code": 2})
app.config.ca.revokeCertificateFromID(crt_id)
except ValueError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except ExpiredCertificate, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except NotFound, e:
raise FlaskException(str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
response = Response("", status=201)
return response
#Manage routes (Authentication required) - Flask APP
# XXX - this routes will be updated or removed after implement ca_user
@app.route('/')
def home():
return redirect(url_for('index'))
@app.route('/certificates')
def index():
# page to list certificates, also connection link
data_list = app.config.ca.getSignedCertificateList()
return render_template("index.html", data_list=data_list)
@app.route('/admin/logout')
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/admin/')
@login_required
def admin():
return index()
@app.route('/admin/csr_requests', methods=['GET'])
@login_required
def manage_csr():
data_list = app.config.ca.getPendingCertificateRequestList()
return render_template('manage_page.html', data_list=data_list)
@app.route('/admin/configure', methods=['GET'])
def configure():
return render_template("configure.html")
@app.route('/admin/setpassword', methods=['POST'])
def setpassword():
username = 'admin'
password = request.form.get('password', '').encode('utf-8')
if not password:
raise FlaskException("'password' parameter is mandatory",
payload={"name": "MissingParameter", "code": 2})
app.config.storage.findOrCreateUser(
"Admin",
"admin",
"admin@example.com",
username,
app.user_manager.hash_password(password))
logout_user()
return redirect(url_for('manage_csr'))
@app.route('/admin/signcert', methods=['GET'])
@login_required
def do_signcert_web():
csr_id = request.args.get('csr_id', '').encode('utf-8')
if not csr_id:
raise FlaskException("'csr_id' parameter is a mandatory parameter",
payload={"name": "MissingParameter", "code": 2})
try:
return signcert(csr_id, subject_dict=None, redirect_to='manage_csr')
except ValueError, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
@app.route('/admin/deletecsr', methods=['GET'])
@login_required
def deletecsr():
"""
Delete certificate signature request file
"""
csr_id = request.args.get('csr_id', '').encode('utf-8')
if not csr_id:
raise FlaskException("'csr_id' parameter is a mandatory parameter",
payload={"name": "MissingParameter", "code": 2})
try:
app.config.ca.deletePendingCertificateRequest(csr_id)
except NotFound, e:
raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
return redirect(url_for('manage_csr'))
@app.route('/admin/profile', methods=['GET', 'POST'])
@login_required
def user_profile():
form = UserProfileForm(request.form, obj=current_user)
if request.method == 'POST' and form.validate():
# Copy form fields to user_profile fields
del form.username # revove username from recieved form
form.populate_obj(current_user)
db.session.commit()
# Redirect to home page
return redirect(url_for('index'))
return render_template('user_profile.html',
form=form)
......@@ -15,42 +15,356 @@
#
# You should have received a copy of the GNU General Public License
# along with caucase. If not, see <http://www.gnu.org/licenses/>.
"""
Caucase - Certificate Authority for Users, Certificate Authority for SErvices
"""
from __future__ import absolute_import
import httplib
import json
import traceback
import sys
from . import utils
from . import exceptions
__all__ = ('Application', )
import os
from caucase import app
from caucase.web import parseArguments, configure_flask
from werkzeug.contrib.fixers import ProxyFix
def _getStatus(code):
return '%i %s' % (code, httplib.responses[code])
def readConfigFromFile(config_file):
config_list = []
with open(config_file) as f:
for line in f.readlines():
if not line or line.startswith('#'):
continue
line_list = line.strip().split(' ')
if len(line_list) == 1:
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:]))
class ApplicationError(Exception):
"""
WSGI HTTP error base class.
"""
status = _getStatus(httplib.INTERNAL_SERVER_ERROR)
response_headers = ()
class BadRequest(ApplicationError):
"""
HTTP bad request error
"""
status = _getStatus(httplib.BAD_REQUEST)
class NotFound(ApplicationError):
"""
HTTP not found error
"""
status = _getStatus(httplib.NOT_FOUND)
class BadMethod(ApplicationError):
"""
HTTP bad method error
"""
status = _getStatus(httplib.METHOD_NOT_ALLOWED)
class Conflict(ApplicationError):
"""
HTTP conflict
"""
status = _getStatus(httplib.CONFLICT)
class TooLarge(ApplicationError):
"""
HTTP too large error
"""
status = _getStatus(httplib.REQUEST_ENTITY_TOO_LARGE)
class InsufficientStorage(ApplicationError):
"""
No storage slot available (not necessarily out of disk space)
"""
# httplib lacks the textual description for 507, although it has the constant...
status = '%i Insufficient Storage' % (httplib.INSUFFICIENT_STORAGE, )
STATUS_OK = _getStatus(httplib.OK)
STATUS_CREATED = _getStatus(httplib.CREATED)
STATUS_NO_CONTENT = _getStatus(httplib.NO_CONTENT)
MAX_BODY_LENGTH = 10 * 1024 * 1024 # 10 MB
class Application(object):
"""
WSGI application class
Thread- and process-safe (locks handled by sqlite3).
"""
def __init__(self, cau, cas):
"""
cau (caucase.ca.CertificateAuthority)
CA for users.
Will be hosted under /cau
return parseArguments(config_list)
cas (caucase.ca.CertificateAuthority)
CA for services.
Will be hosted under /cas
"""
self._cau = cau
self._cas = cas
def start_wsgi():
def __call__(self, environ, start_response):
"""
Start entry for wsgi, do not run app.run, read config from file
WSGI entry point
"""
if os.environ.has_key('CA_CONFIGURATION_FILE'):
config_file = os.environ['CA_CONFIGURATION_FILE']
try: # Convert ApplicationError subclasses in error responses
try: # Convert non-wsgi exceptions into WSGI exceptions
path_item_list = [x for x in environ['PATH_INFO'].split('/') if x]
try:
context_id, method_id = path_item_list[:2]
except ValueError:
raise NotFound
if context_id == 'cau':
context = self._cau
elif context_id == 'cas':
context = self._cas
else:
raise NotFound
if method_id.startswith('_'):
raise NotFound
try:
method = getattr(self, method_id)
except AttributeError:
raise NotFound
status, header_list, result = method(context, environ, path_item_list[2:])
except ApplicationError:
raise
except exceptions.NotFound:
raise NotFound
except exceptions.Found:
raise Conflict
except exceptions.NoStorage:
raise InsufficientStorage
except exceptions.CertificateAuthorityException, e:
raise BadRequest(str(e))
except Exception:
print >>sys.stderr, 'Unhandled exception',
traceback.print_exc(file=sys.stderr)
raise ApplicationError
except ApplicationError, e:
start_response(
e.status,
list(e.response_headers),
)
result = [str(x) for x in e.args]
else:
config_file = 'ca.conf'
start_response(
status,
header_list,
)
return result
@staticmethod
def _read(environ):
"""
Read the entire request body.
configure_flask(readConfigFromFile(config_file))
Raises BadRequest if request Content-Length cannot be parsed.
Raises TooLarge if Content-Length if over MAX_BODY_LENGTH.
If Content-Length is not set, reads at most MAX_BODY_LENGTH bytes.
"""
try:
length = int(environ.get('CONTENT_LENGTH', MAX_BODY_LENGTH))
except ValueError:
raise BadRequest('Invalid Content-Length')
if length > MAX_BODY_LENGTH:
raise TooLarge('Content-Length limit exceeded')
return environ['wsgi.input'].read(length)
def _authenticate(self, environ):
"""
Verify user authentication.
Raises NotFound if authentication does not pass checks.
"""
# Note on NotFound usage here: HTTP specs do not describe how to request
# client to provide transport-level authentication mechanism (x509 cert)
# so 401 is not applicable. 403 is not applicable either as spec requests
# client to not retry the request. 404 is recommended when server does not
# wish to disclose the reason why it rejected the access, so let's use
# this.
try:
ca_list = self._cau.getCACertificateList()
utils.load_certificate(
environ.get('SSL_CLIENT_CERT', b''),
trusted_cert_list=ca_list,
crl=utils.load_crl(
self._cau.getCertificateRevocationList(),
ca_list,
),
)
except (exceptions.CertificateVerificationError, ValueError):
raise NotFound
def _readJSON(self, environ):
"""
Read request body and convert to json object.
Raises BadRequest if request Content-Type is not 'application/json', or if
json decoding fails.
"""
if environ.get('CONTENT_TYPE') != 'application/json':
raise BadRequest('Bad Content-Type')
data = self._read(environ)
try:
return json.loads(data)
except ValueError:
raise BadRequest('Invalid json')
app.wsgi_app = ProxyFix(app.wsgi_app)
@staticmethod
def crl(context, environ, subpath):
"""
Handle /{context}/crl urls.
"""
if subpath:
raise NotFound
if environ['REQUEST_METHOD'] != 'GET':
raise BadMethod
data = context.getCertificateRevocationList()
return (
STATUS_OK,
[
('Content-Type', 'application/pkix-crl'),
('Content-Length', str(len(data))),
],
[data],
)
app.logger.info("Certificate Authority server ready...")
def csr(self, context, environ, subpath):
"""
Handle /{context}/csr urls.
"""
http_method = environ['REQUEST_METHOD']
if http_method == 'GET':
if subpath:
try:
csr_id, = subpath
except ValueError:
raise NotFound
try:
csr_id = int(csr_id)
except ValueError:
raise BadRequest('Invalid integer')
data = context.getCertificateSigningRequest(csr_id)
content_type = 'application/pkcs10'
else:
self._authenticate(environ)
data = json.dumps(context.getCertificateRequestList())
content_type = 'application/json'
return (
STATUS_OK,
[
('Content-Type', content_type),
('Content-Length', str(len(data))),
],
[data],
)
elif http_method == 'PUT':
if subpath:
raise NotFound
csr_id = context.appendCertificateSigningRequest(self._read(environ))
return (
STATUS_CREATED,
[
('Location', str(csr_id)),
],
[],
)
elif http_method == 'DELETE':
try:
csr_id, = subpath
except ValueError:
raise NotFound
self._authenticate(environ)
try:
context.deletePendingCertificateSigningRequest(csr_id)
except exceptions.NotFound:
raise NotFound
return (STATUS_NO_CONTENT, [], [])
else:
raise BadMethod
if __name__ == 'caucase.wsgi':
start_wsgi()
def crt(self, context, environ, subpath):
"""
Handle /{context}/crt urls.
"""
http_method = environ['REQUEST_METHOD']
try:
crt_id, = subpath
except ValueError:
raise NotFound
if http_method == 'GET':
if crt_id == 'ca.crt.pem':
data = context.getCACertificate()
content_type = 'application/pkix-cert'
elif crt_id == 'ca.crt.json':
data = json.dumps(context.getValidCACertificateChain())
content_type = 'application/json'
else:
try:
crt_id = int(crt_id)
except ValueError:
raise BadRequest('Invalid integer')
data = context.getCertificate(crt_id)
content_type = 'application/pkix-cert'
return (
STATUS_OK,
[
('Content-Type', content_type),
('Content-Length', str(len(data))),
],
[data],
)
elif http_method == 'PUT':
if crt_id == 'renew':
payload = utils.unwrap(
self._readJSON(environ),
lambda x: x['crt_pem'],
context.digest_list,
)
data = context.renew(
crt_pem=payload['crt_pem'].encode('ascii'),
csr_pem=payload['renew_csr_pem'].encode('ascii'),
)
return (
STATUS_OK,
[
('Content-Type', 'application/pkix-cert'),
('Content-Length', str(len(data))),
],
[data],
)
elif crt_id == 'revoke':
data = self._readJSON(environ)
if data['digest'] is None:
self._authenticate(environ)
payload = utils.nullUnwrap(data)
if 'revoke_crt_pem' not in payload:
context.revokeSerial(payload['revoke_serial'])
return (STATUS_NO_CONTENT, [], [])
else:
payload = utils.unwrap(
data,
lambda x: x['revoke_crt_pem'],
context.digest_list,
)
context.revoke(
crt_pem=payload['revoke_crt_pem'].encode('ascii'),
)
return (STATUS_NO_CONTENT, [], [])
else:
try:
crt_id = int(crt_id)
except ValueError:
raise BadRequest('Invalid integer')
body = self._read(environ)
if not body:
template_csr = None
elif environ.get('CONTENT_TYPE') == 'application/pkcs10':
template_csr = utils.load_certificate_request(body)
else:
raise BadRequest('Bad Content-Type')
self._authenticate(environ)
context.createCertificate(
csr_id=crt_id,
template_csr=template_csr,
)
return (STATUS_NO_CONTENT, [], [])
else:
raise BadMethod
......@@ -9,10 +9,10 @@ autonumber
== Signing Request Submission ==
User -> Authority : POST the CRL
alt Sining request passes all checks
Authority --> User : Signing request identifier
else
User -> Authority : PUT /csr with the CSR as body
alt CSR passes format check
Authority --> User : Request identifier
else CSR format invalid
Authority --> User : Error
end
Note over User : See "Certificate Retrieval"
......@@ -20,18 +20,18 @@ Note over User : See "Certificate Retrieval"
== Certificate Production ==
Note over Trusted : See "Signing Request Submission"
Trusted -> Authority : GET (optional: with result range expression)
Trusted -> Authority : GET /csr
Authority --> Trusted : List of pending signing requests with their identifiers
Trusted -> Authority : GET with request identifier
Authority --> Trusted : Signing request content
alt Trusted agrees to prvoduce a signed certificate from the signing request
Trusted -> Authority : POST with the signing request identifier
alt Sining request was still pending
Trusted -> Authority : GET /csr/<request identifier>
Authority --> Trusted : CSR
alt Trusted agrees to produce a signed certificate from the signing request
Trusted -> Authority : PUT /crt/<request identifier>
alt CSR was still pending
Authority --> Trusted : Success
else
else CSR not pending (deleted or already signed)
Authority --> Trusted : Not found
end
else
else Trusted refuses to sign the request
Trusted -> Authority : DELETE with the signing request identifier
Authority --> Trusted : Ok
end
......@@ -39,16 +39,16 @@ end
== Certificate Retrieval ==
loop Until certificate obtained or request rejected
User -> Authority : GET with signing request identifier
alt Signing request was signed
User -> Authority : GET /crt/<request identifier>
alt CRT exists
Authority --> User : Certificate content
else
else CRT does not exist
Authority --> User : Not found
opt User wants to check request is still pending
User -> Authority : GET with signing request identifier
alt Sining request is still pending
opt User checks if the CSR was rejected
User -> Authority : GET /csr/<request identifier>
alt CSR still pending
Authority --> User : Signing request content
else
else CSR rejected
Authority --> User : Not found
end
end
......@@ -57,25 +57,43 @@ end
== Certificate Renewal ==
User -> Authority : POST with the still-valid CRT and a CRL
alt Renewal parameters consistent
Authority --> User : signing request identifier
else
User -> Authority : PUT /crt/renew with the still-valid CRT and a CRL with the new public key
alt CRT is still valid (validity period, not revoked)
Authority --> User : New certificate content
else CRT invalid
Authority --> User : Error
end
Note over User : See "Certificate Retrieval"
== Certificate Revocation ==
User -> Authority : POST with the CRT
alt Revocation parameters consistent
Authority --> User : Ok
else
User -> Authority : PUT /crt/revoke with the CRT, order signed with its private key
alt CRT is valid and parameters consistent
Authority --> User : CRT revoked
else CRT is invalid or parameters inconsistent
Authority --> User : Error
end
== Certificate Revocation without access to private key ==
Trusted -> Authority : PUT /crt/revoke with the CRT
alt CRT is valid
Authority --> Trusted : CRT revoked
else CRT is invalid
Authority --> Trusted : Error
end
== Certificate Revocation without access to private key or the certificate ==
Note over Trusted: This procedure is discouraged as revoked certificate will linger for much longer\nthan strictly needed in the CRL, but useful when only the bare minimum\nis know about the certificate which revocation is desired.
Trusted -> Authority : PUT /crt/revoke with the serial to revoke
alt Serial is not revoked yet
Authority --> Trusted : CRT revoked
else Serials is already revoked
Authority --> Trusted : Error
end
== Certificate Validity Check ==
Service -> Authority : GET (optional: with OCSP parameter)
Authority --> Service : Certificate revocation list
Service -> Authority : GET /crl
Authority --> Service : CRL content
@enduml
swagger: '2.0'
info:
title: Certificate Authority
description: Automated x509 certificate authority for use with non-http services
version: 0.0.1
title: caucase
description: Certificate Authority for Users, Certificate Authority for SErvices
version: 0.2.0
contact:
name: Vincent Pelletier (Nexedi)
url: 'http://www.nexedi.com'
......@@ -13,33 +13,29 @@ schemes:
- https
consumes:
- application/json
- application/pkcs10
produces:
- application/xml
- application/json
- application/pkix-cert
- application/pkix-crl
- application/pkcs10
- application/x-x509-ca-cert
tags:
- name: todo
description: Known as not fully specified yet
- name: auth
description: https client authentication required
paths:
/csr:
get:
tags:
- todo
summary: List pending certificate signing requests - XXX PROPFIND, not GET !
summary: List pending certificate signing requests
operationId: getPendingCertificateRequestList
schemes:
- https
parameters:
- $ref: '#/parameters/x-auth'
tags:
- auth
produces:
- application/json
responses:
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'200':
description: OK - CSR list returned
'404':
$ref: '#/responses/404'
put:
summary: Request a new certificate signature
operationId: createCertificateSigningRequest
......@@ -54,32 +50,21 @@ paths:
Location:
description: URL of created resource
type: string
'400':
$ref: '#/responses/400'
'500':
$ref: '#/responses/500'
'507':
$ref: '#/responses/507'
/csr/{crt-id}:
delete:
summary: Reject a pending certificate signing request
operationId: deletePendingCertificateRequest
schemes:
- https
tags:
- auth
parameters:
- $ref: '#/parameters/x-auth'
- $ref: '#/parameters/crt-id'
responses:
'204':
description: No Content - CSR was rejected
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
description: No Content - CSR was successfuly rejected
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
get:
summary: Retrieve a pending certificate signing request
operationId: getCertificateSigningRequest
......@@ -90,37 +75,21 @@ paths:
responses:
'200':
description: OK - CSR retrieved
'400':
$ref: '#/responses/400'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/crt:
/crt/{crt-id}:
put:
summary: Accept pending certificate signing request
operationId: createCertificate
schemes:
- https
consumes:
- application/pkcs10
parameters:
- $ref: '#/parameters/x-auth'
- $ref: '#/parameters/csr'
tags:
- auth
responses:
'201':
description: Created - Certificate was signed
headers:
Location:
description: URL of created resource
type: string
'401':
$ref: '#/responses/401'
'204':
description: No Content - CSR was successfuly signed
'404':
$ref: '#/responses/404'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
/crt/{crt-id}:
get:
summary: Retrieve a signed certificate
operationId: getCertificate
......@@ -133,81 +102,62 @@ paths:
description: OK - CRT retrieved
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/crt/ca.crt.pem:
get:
summary: Retrieve current CA certificate
operationId: getCACertificate
produces:
- application/x-x509-ca-cert
responses:
'200':
description: OK - CA CRT retrieved
/crt/ca.crt.json:
get:
summary: Retrieve current CA certificate trust chain
description: Response schema is described separately.
operationId: getCACertificateChain
produces:
- application/json
responses:
'200':
description: OK - CA CRT chain retrieved
/crt/revoke:
put:
summary: Revoke a certificate
description: >
Response status 400 may mean operation could not be authenticated (signature hash mechanism not supported by server, signature did not match...). Signed operation payload schema is described separately.
description: Signed operation payload schema is described separately.
operationId: revokeCertificate
consumes:
- application/json
parameters:
- $ref: '#/parameters/signed-operation'
responses:
'201':
description: Created - Signing request was revoked
headers:
Location:
description: URL of created resource
type: string
'400':
$ref: '#/responses/400'
'500':
$ref: '#/responses/500'
'204':
description: No Content - certificate revoked
/crt/renew:
put:
summary: Renew a certificate
description: >
Response status 400 may mean operation could not be authenticated (signature hash mechanism not supported by server, signature did not match...), in addition to createCertificateSigningRequest status 400 reasons. Signed operation payload schema is described separately.
description: Signed operation payload schema is described separately.
operationId: renewCertificate
consumes:
- application/json
parameters:
- $ref: '#/parameters/signed-operation'
responses:
'201':
description: Created - Renewwal request was accepted
headers:
Location:
description: URL of created resource
type: string
'400':
$ref: '#/responses/400'
'500':
$ref: '#/responses/500'
'200':
description: OK - Renewed certificate retrieved
/crl:
get:
summary: Retrieve latest certificate revocation list
operationId: getCertificateRevocationList
produces:
- application/x-x509-ca-cert
- application/pkix-crl
responses:
'200':
description: OK - CRL retrieved
'500':
$ref: '#/responses/500'
definitions:
csr:
type: string
description: application/pkcs10 data
Error:
type: object
required:
- code
- name
- message
properties:
code:
type: integer
description: application-specific error code
name:
type: string
description: the explicit error name
message:
type: string
description: describes the error
signed-operation:
type: object
required:
......@@ -231,12 +181,6 @@ parameters:
description: Opaque certificate signing request identifier
required: true
type: string
x-auth:
name: Authorization
in: header
description: Credentials for this request
type: string
required: true
csr:
name: csr
in: body
......@@ -253,25 +197,7 @@ parameters:
responses:
'400':
description: Bad Request - you probably provided wrong parameters
schema:
$ref: '#/definitions/Error'
'401':
description: Unauthorized - No Authorization header provided
headers:
Authenticate:
type: string
description: Requests that client provides credentials
schema:
$ref: '#/definitions/Error'
'403':
description: Forbidden - Authorization headed provided, but does not grant access
schema:
$ref: '#/definitions/Error'
'404':
description: Not Found - Requested resource does not exist
schema:
$ref: '#/definitions/Error'
'500':
description: Internal Server Error
description: Not Found - Requested resource does not exist, or you did not provide required transport-level credentials (x509 cert over https)
'507':
description: Insufficient Storage
......@@ -20,60 +20,49 @@ from setuptools import setup, find_packages
import glob
import os
version = '0.1.4'
name = 'caucase'
long_description = open("README.rst").read() + "\n"
for f in sorted(glob.glob(os.path.join('caucase', 'README.*.rst'))):
long_description += '\n' + open(f).read() + '\n'
# long_description += open("CHANGES.txt").read() + "\n"
# Provide a way to install additional requirements
additional_install_requires = []
try:
import argparse
except ImportError:
additional_install_requires.append('argparse')
setup(name=name,
version=version,
setup(
name='caucase',
version='0.9.0',
description="Certificate Authority.",
long_description=long_description,
classifiers=[
"Programming Language :: Python",
'Environment :: Console',
'Environment :: Web Environment',
'Intended Audience :: System Administrators',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Topic :: Security :: Cryptography',
'Topic :: System :: Systems Administration :: Authentication/Directory',
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
],
keywords='certificate authority',
url='https://lab.nexedi.com/nexedi/caucase',
license='GPLv3',
namespace_packages=['caucase'],
license='GPLv3+',
packages=find_packages(),
include_package_data=True,
install_requires=[
'Flask', # needed by servers
'flask_user',
'Flask-AlchemyDumps',
'setuptools', # namespaces
'pyOpenSSL', # manage ssl certificates
'pyasn1', # ASN.1 types and codecs for certificates
'pyasn1-modules',
'requests', # http requests
'cryptography', # everything x509 except...
'pyOpenSSL', # ...certificate chain validation
'pem', # Parse PEM files
] + additional_install_requires,
extras_require = {
},
tests_require = [
'Flask-Testing',
],
zip_safe=False, # proxy depends on Flask, which has issues with
# accessing templates
tests_require=[
'coverage',
'pylint',
],
zip_safe=True,
entry_points={
'console_scripts': [
'caucase = caucase.web:start',
'caucase-cli = caucase.cli:main',
'caucase-cliweb = caucase.cli_flask:main',
'caucase = caucase.cli:main',
'caucase-probe = caucase.cli:probe',
'caucase-monitor = caucase.cli:monitor',
'caucase-rerequest = caucase.cli:rerequest',
'caucase-key-id = caucase.cli:key_id',
'caucased = caucase.http:main',
]
},
test_suite='caucase.test',
)
)
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