...
 
Commits (10)
[MASTER]
ignore=_version.py
[DESIGN]
max-args=12
max-nested-blocks=6
max-module-lines=1500
[MESSAGES CONTROL]
disable=C0103,C0330,R0903,R0913,R0914,R0912,R0915,R0902
disable=C0103,C0330,R0903,R0914,R0912,R0915,R0902
# C0103 "Invalid name "%s" (should match %s)"
# C0330 bad-continuation
# R0903 too-few-public-methods
# R0913 too-many-arguments
# R0914 too-many-locals
# R0912 too-many-branches
# R0915 too-many-statements
......
0.9.4 (2018-11-14)
==================
* Improved documentation.
* Tentative web-friendliness (not used in real life yet, so practicality is still uncertain):
* Make caucased https CA certificate safer for adding in a trust store (ex: browser) by constraining the certificates it can sign.
* cookie-based CORS access control with crude UI.
* API is self-documenting using application/hal+json format.
* Tentative python3 friendliness, there may still be file IO encoding issues.
0.9.3 (2018-09-21)
==================
* Add support for listening to multiple specific addresses in caucased.
......
......@@ -291,9 +291,6 @@ class CertificateAuthority(object):
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,
......@@ -311,7 +308,9 @@ class CertificateAuthority(object):
critical=True, # "MAY appear as critical or non-critical"
),
Extension(
subject_key_identifier,
x509.SubjectKeyIdentifier.from_public_key(
public_key,
),
critical=False, # "MUST mark this extension as non-critical"
),
Extension(
......
......@@ -40,7 +40,14 @@ from .client import (
)
class RetryingCaucaseClient(CaucaseClient):
_until = utils.until
"""
Similar to CaucaseClient, but retries indefinitely on http & socket errors.
To use in long-lived processes where server may not be available yet, or is
(hopefuly) temporarily unavailable, etc.
Retries every 10 seconds.
"""
_until = staticmethod(utils.until)
def _request(self, connection, method, url, body=None, headers=None):
while True:
......@@ -95,7 +102,10 @@ class CLICaucaseClient(object):
csr_pem = utils.getCertRequest(csr_path)
# Quick sanity check
utils.load_certificate_request(csr_pem)
print(self._client.createCertificateSigningRequest(csr_pem), csr_path)
print(
self._client.createCertificateSigningRequest(csr_pem),
utils.toBytes(csr_path),
)
def getCSR(self, csr_id_path_list):
"""
......@@ -122,10 +132,10 @@ class CLICaucaseClient(object):
except CaucaseError as e:
if e.args[0] != httplib.NOT_FOUND:
raise
print(crt_id, 'not found - maybe CSR was rejected ?')
print(crt_id, b'not found - maybe CSR was rejected ?')
error = True
else:
print(crt_id, 'CSR still pending')
print(crt_id, b'CSR still pending')
warning = True
else:
print(crt_id, end=' ')
......@@ -134,15 +144,15 @@ class CLICaucaseClient(object):
ca_list,
None,
)):
print('was (originally) automatically approved')
print(b'was (originally) automatically approved')
else:
print('was (originally) manually approved')
print(b'was (originally) manually approved')
if os.path.exists(crt_path):
try:
key_pem = utils.getKey(crt_path)
except ValueError:
print(
'Expected to find exactly one privatekey key in %s, skipping' % (
b'Expected to find exactly one privatekey key in %s, skipping' % (
crt_path,
),
file=sys.stderr,
......@@ -153,7 +163,7 @@ class CLICaucaseClient(object):
utils.validateCertAndKey(crt_pem, key_pem)
except ValueError:
print(
'Key in %s does not match retrieved certificate, skipping',
b'Key in %s does not match retrieved certificate, skipping',
file=sys.stderr,
)
error = True
......@@ -171,7 +181,7 @@ class CLICaucaseClient(object):
crt, key, _ = utils.getKeyPair(crt_path, key_path)
except ValueError:
print(
'Could not find (exactly) one matching key pair in %s, skipping' % (
b'Could not find (exactly) one matching key pair in %s, skipping' % (
[x for x in set((crt_path, key_path)) if x],
),
file=sys.stderr,
......@@ -201,7 +211,7 @@ class CLICaucaseClient(object):
)
except ValueError:
print(
'Could not find (exactly) one matching key pair in %s, skipping' % (
b'Could not find (exactly) one matching key pair in %s, skipping' % (
[x for x in set((crt_path, key_path)) if x],
),
file=sys.stderr,
......@@ -217,11 +227,11 @@ class CLICaucaseClient(object):
except exceptions.CertificateVerificationError:
print(
crt_path,
'was not signed by this CA, revoked or otherwise invalid, skipping',
b'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')
print(crt_path, b'did not reach renew threshold, not renewing')
continue
new_key_pem, new_crt_pem = self._client.renewCertificate(
old_crt=old_crt,
......@@ -249,22 +259,22 @@ class CLICaucaseClient(object):
"""
--list-csr
"""
print('-- pending', mode, 'CSRs --')
print(b'-- pending', mode, b'CSRs --')
print(
'%20s | %s' % (
'csr_id',
'subject preview (fetch csr and check full content !)',
b'%20s | %s' % (
b'csr_id',
b'subject preview (fetch csr and check full content !)',
),
)
for entry in self._client.getPendingCertificateRequestList():
csr = utils.load_certificate_request(utils.toBytes(entry['csr']))
print(
'%20s | %r' % (
entry['id'],
csr.subject,
b'%20s | %r' % (
utils.toBytes(entry['id']),
utils.toBytes(repr(csr.subject)),
),
)
print('-- end of pending', mode, 'CSRs --')
print(b'-- end of pending', mode, b'CSRs --')
def signCSR(self, csr_id_list):
"""
......@@ -308,7 +318,7 @@ class CLICaucaseClient(object):
crt_pem = utils.getCert(crt_path)
except ValueError:
print(
'Could not load a single certificate in %s, skipping' % (
b'Could not load a single certificate in %s, skipping' % (
crt_path,
),
file=sys.stderr,
......@@ -524,8 +534,8 @@ def main(argv=None):
sign_csr_id_set.intersection(sign_with_csr_id_set)
):
print(
'A given CSR_ID cannot be in more than one of --sign-csr, '
'--sign-csr-with and --reject-csr',
b'A given CSR_ID cannot be in more than one of --sign-csr, '
b'--sign-csr-with and --reject-csr',
file=sys.stderr,
)
raise SystemExit(STATUS_ERROR)
......@@ -751,12 +761,12 @@ def updater(argv=None, until=utils.until):
ca_crt_pem_list=utils.getCertList(args.cas_ca)
)
if args.crt and not utils.hasOneCert(args.crt):
print('Bootstraping...')
print(b'Bootstraping...')
csr_pem = utils.getCertRequest(args.csr)
# Quick sanity check before bothering server
utils.load_certificate_request(csr_pem)
csr_id = client.createCertificateSigningRequest(csr_pem)
print('Waiting for signature of', csr_id)
print(b'Waiting for signature of', csr_id)
while True:
try:
crt_pem = client.getCertificate(csr_id)
......@@ -774,12 +784,12 @@ def updater(argv=None, until=utils.until):
crt_file.write(crt_pem)
updated = True
break
print('Bootstrap done')
print(b'Bootstrap done')
next_deadline = datetime.datetime.utcnow()
while True:
print(
'Next wake-up at',
next_deadline.strftime('%Y-%m-%d %H:%M:%S +0000'),
b'Next wake-up at',
next_deadline.strftime(b'%Y-%m-%d %H:%M:%S +0000'),
)
now = until(next_deadline)
next_deadline = now + max_sleep
......@@ -792,7 +802,7 @@ def updater(argv=None, until=utils.until):
ca_crt_pem_list=utils.getCertList(args.cas_ca)
)
if RetryingCaucaseClient.updateCAFile(ca_url, args.ca):
print('Got new CA')
print(b'Got new CA')
updated = True
# Note: CRL expiration should happen several time during CA renewal
# period, so it should not be needed to keep track of CA expiration
......@@ -802,18 +812,18 @@ def updater(argv=None, until=utils.until):
for x in utils.getCertList(args.ca)
]
if RetryingCaucaseClient.updateCRLFile(ca_url, args.crl, ca_crt_list):
print('Got new CRL')
print(b'Got new CRL')
updated = True
with open(args.crl, 'rb') as crl_file:
next_deadline = min(
next_deadline,
utils.load_crl(crli_file.read(), ca_crt_list).next_update,
utils.load_crl(crl_file.read(), ca_crt_list).next_update,
)
if args.crt:
crt_pem, key_pem, key_path = utils.getKeyPair(args.crt, args.key)
crt = utils.load_certificate(crt_pem, ca_crt_list, None)
if crt.not_valid_after - threshold <= now:
print('Renewing', args.crt)
print(b'Renewing', args.crt)
new_key_pem, new_crt_pem = client.renewCertificate(
old_crt=crt,
old_key=utils.load_privatekey(key_pem),
......@@ -843,7 +853,7 @@ def updater(argv=None, until=utils.until):
if args.on_renew is not None:
status = os.system(args.on_renew)
if status:
print('Renewal hook exited with status:', status, file=sys.stderr)
print(b'Renewal hook exited with status:', status, file=sys.stderr)
raise SystemExit(STATUS_ERROR)
updated = False
except (utils.SleepInterrupt, SystemExit):
......@@ -954,4 +964,4 @@ def key_id(argv=None):
backup_file.read(struct.calcsize('<I')),
)
for key_entry in json.loads(backup_file.read(header_len))['key_list']:
print(' ', key_entry['id'])
print(b' ', key_entry['id'])
......@@ -182,7 +182,7 @@ class CaucaseSSLWSGIRequestHandler(CaucaseWSGIRequestHandler):
# Note: compared to BaseHTTPHandler, logs the client certificate serial as
# user name.
print(
"%s - %s [%s] %s" % (
'%s - %s [%s] %s' % (
self.client_address[0],
self.ssl_client_cert_serial,
self.log_date_time_string(),
......@@ -226,15 +226,13 @@ def getSSLContext(
# 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.
# Note: python's standard ssl module does not provide a way to replace the
# current CRL file on an existing openssl context: load_verify_locations ends
# up calling X509_STORE_add_crl, which either adds the CRL to its list of
# files or rejects the file. So either memory usage with increase until
# context gets renewed, or we get stuck with an old CRL. So expect wsgi
# application to implement these checks on its own when accessing client's
# certificate.
#ssl_context.verify_flags = ssl.VERIFY_CRL_CHECK_LEAF
ssl_context.load_verify_locations(
cadata=utils.toUnicode(cau.getCACertificate()),
......@@ -954,11 +952,11 @@ def manage(argv=None):
found_from = ', '.join(ca_pair['from'])
crt = ca_pair['crt']
if crt is None:
print('No certificate correspond to ' + found_from + ', skipping')
print(b'No certificate correspond to', found_from, b'- skipping')
continue
expiration = utils.datetime2timestamp(crt.not_valid_after)
if expiration < now:
print('Skipping expired certificate from ' + found_from)
print(b'Skipping expired certificate from', found_from)
del import_ca_dict[identifier]
continue
if not args.import_bad_ca:
......@@ -977,11 +975,11 @@ def manage(argv=None):
or not key_usage.key_cert_sign or not key_usage.crl_sign
)
if failed:
print('Skipping non-CA certificate from ' + found_from)
print(b'Skipping non-CA certificate from', found_from)
continue
key = ca_pair['key']
if key is None:
print('No private key correspond to ' + found_from + ', skipping')
print(b'No private key correspond to', found_from, b'- skipping')
continue
imported += 1
cas_db.appendCAKeyPair(
......@@ -993,7 +991,7 @@ def manage(argv=None):
)
if not imported:
raise ValueError('No CA certificate imported')
print('Imported %i CA certificates' % imported)
print(b'Imported %i CA certificates' % imported)
if args.import_crl:
db = SQLite3Storage(db_path, table_prefix='cas')
trusted_ca_crt_set = [
......@@ -1016,7 +1014,7 @@ def manage(argv=None):
already_revoked_count += 1
else:
revoked_count += 1
print('Revoked %i certificates (%i were already revoked)' % (
print(b'Revoked %i certificates (%i were already revoked)' % (
revoked_count,
already_revoked_count,
))
......
......@@ -20,6 +20,7 @@ Caucase - Certificate Authority for Users, Certificate Authority for SErvices
Test suite
"""
# pylint: disable=too-many-lines, too-many-public-methods
from __future__ import absolute_import
from Cookie import SimpleCookie
import datetime
......@@ -46,8 +47,11 @@ from urllib import quote, urlencode
import urlparse
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from caucase import cli
from caucase.ca import Extension
from caucase.client import CaucaseError, CaucaseClient
from caucase.exceptions import CertificateVerificationError
# Do not import caucase.http into this namespace: 2to3 will import standard
# http module, which will then be masqued by caucase's http submodule.
import caucase.http
......@@ -148,8 +152,13 @@ class FakeStreamRequest(object):
"""
return self._rfile if 'r' in mode else self._wfile
# pylint: disable=unused-argument
def sendall(self, data, flags=None): # pragma: no cover
"""
Redirect sendall.
"""
self._wfile.write(data)
# pylint: enable=unused-argument
class NoCloseFileProxy(object):
"""
......@@ -319,6 +328,160 @@ class CaucaseTest(unittest.TestCase):
self._stopServer()
shutil.rmtree(self._data_dir)
@staticmethod
def _getCAKeyPair(extension_list=(), not_before=None, not_after=None):
"""
Build a reasonably-realistic CA, return key & self-signed cert.
"""
if not_before is None:
not_before = datetime.datetime.utcnow()
if not_after is None:
not_after = not_before + datetime.timedelta(10, 0)
private_key = utils.generatePrivateKey(2048)
subject = x509.Name([
x509.NameAttribute(
oid=x509.oid.NameOID.COMMON_NAME,
value=u'John Doe CA',
),
])
public_key = private_key.public_key()
subject_key_identifier = x509.SubjectKeyIdentifier.from_public_key(
public_key,
)
return private_key, x509.CertificateBuilder(
subject_name=subject,
issuer_name=subject,
not_valid_before=not_before,
not_valid_after=not_after,
serial_number=x509.random_serial_number(),
public_key=public_key,
extensions=[
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"
),
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"
),
] + list(extension_list),
).sign(
private_key=private_key,
algorithm=hashes.SHA256(),
backend=_cryptography_backend,
)
@staticmethod
def _getKeyPair(
ca_key,
ca_crt,
extension_list=(),
not_before=None,
not_after=None,
):
"""
Build a reasonably-realistic signed cert, return key & self-signed cert.
"""
if not_before is None:
not_before = datetime.datetime.utcnow()
if not_after is None:
not_after = not_before + datetime.timedelta(10, 0)
crt_key = utils.generatePrivateKey(2048)
return crt_key, x509.CertificateBuilder(
subject_name=x509.Name([
x509.NameAttribute(
oid=x509.oid.NameOID.ORGANIZATIONAL_UNIT_NAME,
value=u'Jane Doe',
),
]),
issuer_name=ca_crt.subject,
not_valid_before=not_before,
not_valid_after=not_after,
serial_number=x509.random_serial_number(),
public_key=crt_key.public_key(),
extensions=[
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,
),
] + list(extension_list),
).sign(
private_key=ca_key,
algorithm=hashes.SHA256(),
backend=_cryptography_backend,
)
def _skipIfOpenSSLDoesNotSupportIPContraints(self):
ca_key, ca_crt = self._getCAKeyPair(
extension_list=[
Extension(
x509.NameConstraints(
permitted_subtrees=[
x509.IPAddress(ipaddress.ip_network(u'127.0.0.1')),
],
excluded_subtrees=None,
),
critical=True,
),
],
)
_, crt = self._getKeyPair(
ca_key=ca_key,
ca_crt=ca_crt,
)
try:
# pylint: disable=protected-access
utils._verifyCertificateChain(
cert=crt,
trusted_cert_list=[ca_crt],
crl=None,
)
# pylint: enable=protected-access
except CertificateVerificationError:
raise unittest.SkipTest('OpenSSL versoin does not support IP constraints')
def _restoreServer(
self,
backup_path,
......@@ -398,7 +561,7 @@ class CaucaseTest(unittest.TestCase):
Returns stdout.
"""
orig_stdout = sys.stdout
sys.stdout = stdout = StringIO()
sys.stdout = stdout = BytesIO()
try:
cli.main(
argv=(
......@@ -1329,6 +1492,9 @@ class CaucaseTest(unittest.TestCase):
))
server_key_file.write(ca_crt_pem)
def readServerKey():
"""
Read server key from file.
"""
with open(self._server_key, 'rb') as server_key_file:
return server_key_file.read()
reference_server_key = readServerKey()
......@@ -2194,7 +2360,7 @@ class CaucaseTest(unittest.TestCase):
orig_stdout = sys.stdout
try:
caucase.http.getBytePass = lambda x: b'test'
sys.stdout = stdout = StringIO()
sys.stdout = stdout = BytesIO()
self.assertFalse(os.path.exists(exported_ca), exported_ca)
caucase.http.manage(
argv=(
......@@ -2558,19 +2724,18 @@ class CaucaseTest(unittest.TestCase):
after = ssl.get_server_certificate(address)
self.assertNotEqual(before, after)
def testHttpNetlocIPv6(self):
def _testHttpCustomNetLoc(self, netloc):
"""
Test that it is possible to use a literal IPv6 as netloc.
This used to fail because cryptography module would reject bare IPv6
address in CRL distribution point extension.
Breaks on OpenSSL < 1.1.0 as it lacks support for validating
certificates with IP constraints.
"""
self._skipIfOpenSSLDoesNotSupportIPContraints()
self._stopServer()
os.unlink(self._server_key)
os.unlink(self._server_db)
netloc = '[::1]'
port = urlparse.urlparse(self._caucase_url).port
if port:
netloc += ':%s' % port
netloc += ':%s' % port
self._server_netloc = netloc
self._caucase_url = 'http://' + netloc
self._startServer()
......@@ -2592,6 +2757,20 @@ class CaucaseTest(unittest.TestCase):
uri, = distribution_point.full_name
self.assertEqual(uri.value, self._caucase_url + u'/cas/crl')
def testHttpNetlocIPv4(self):
"""
Test that it is possible to use a literal IPv4 as netloc.
"""
self._testHttpCustomNetLoc(netloc='127.0.0.1')
def testHttpNetlocIPv6(self):
"""
Test that it is possible to use a literal IPv6 as netloc.
This used to fail because cryptography module would reject bare IPv6
address in CRL distribution point extension (unlike IPv4).
"""
self._testHttpCustomNetLoc(netloc='[::1]')
def testServerFilePermissions(self):
"""
Check that both the sqlite database and server keys are group- and
......@@ -2602,10 +2781,12 @@ class CaucaseTest(unittest.TestCase):
self.assertEqual(os.stat(self._server_db).st_mode & 0o777, 0o600)
self.assertEqual(os.stat(self._server_key).st_mode & 0o777, 0o600)
# pylint: disable=no-member
if getattr(CaucaseTest, 'assertItemsEqual', None) is None:
# Because python3 decided it should be named differently, and 2to3 cannot
# pick it up, and this code must remain python2-compatible... Yay !
CaucaseTest.assertItemsEqual = CaucaseTest.assertCountEqual
# pylint: enable=no-member
if __name__ == '__main__': # pragma: no cover
unittest.main()
......@@ -28,16 +28,18 @@ import threading
import time
import traceback
from urllib import quote, urlencode
from urlparse import parse_qs, urlparse, urlunparse
from urlparse import parse_qs
from wsgiref.util import application_uri, request_uri
import jwt
from . import utils
from . import exceptions
# pylint: disable=import-error
if sys.version_info >= (3, ): # pragma: no cover
from html import escape
else: # pragma: no cover
from cgi import escape
# pylint: enable=import-error
__all__ = ('Application', 'CORSTokenManager')
......