Commit 809c04cd authored by Alain Takoudjou's avatar Alain Takoudjou

allow to set custom subject when signing certificate

parent 52d85d1e
...@@ -42,6 +42,22 @@ MIN_CA_RENEW_PERIOD = 2 ...@@ -42,6 +42,22 @@ MIN_CA_RENEW_PERIOD = 2
DEFAULT_DIGEST_LIST = ['sha256', 'sha384', 'sha512'] DEFAULT_DIGEST_LIST = ['sha256', 'sha384', 'sha512']
SUBJECT_KEY_LIST = ['C', 'ST', 'L', 'OU', 'O', 'CN', 'emailAddress'] SUBJECT_KEY_LIST = ['C', 'ST', 'L', 'OU', 'O', 'CN', 'emailAddress']
def x509_name(**attrs):
"""
Return a new X509Name with the given attributes.
"""
# XXX There's no other way to get a new X509Name.
name = crypto.X509().get_subject()
attrs = list(attrs.items())
  • If we still want to sort ourselves (see comment below), then this and sorting could be merged into a single statement:

    attrs = sorted(attrs.items(), key=lambda x: x[1])

    BTW, I am surprised:

    • by the variable naming, which does not follow our usual naming conventions
    • by the variable type mutation (dict becomes list ?) which, without being invalid, is error-prone
    • by the fact we sort by value (index 1) and not by key (index 0): is it really what is intended ?
Please register or sign in to reply
# Make the order stable - order matters!
  • If order matters, then shouldn't this function take a list of items instead of a mapping, so caller can control the order ?

    Or can we impose the order reliably here ? (but then, why doesn't openssl do it internally if it is so trivial ?)

  • @vpelletier It was made in a sample in pyopenssl repositorie like that, I didn't check so much if order would have caused a problem. But now that I checked it seems not. I remove the part to sort items but the result still the same, no matter the order it works. Maybe it was like this with an older version of pyopenssl. I commited changes on abe15200

Please register or sign in to reply
def key(attr):
return attr[1]
attrs.sort(key=key)
for k, v in attrs:
setattr(name, k, v)
return name
class CertificateAuthority(object): class CertificateAuthority(object):
def __init__(self, storage, ca_life_period, ca_renew_period, def __init__(self, storage, ca_life_period, ca_renew_period,
crt_life_time, crl_renew_period, digest_list=None, crt_life_time, crl_renew_period, digest_list=None,
...@@ -210,7 +226,7 @@ class CertificateAuthority(object): ...@@ -210,7 +226,7 @@ class CertificateAuthority(object):
""" """
return self._storage.getPendingCertificateRequestList(limit, with_data) return self._storage.getPendingCertificateRequestList(limit, with_data)
def createCertificate(self, csr_id, ca_key_pair=None): def createCertificate(self, csr_id, ca_key_pair=None, subject_dict=None):
""" """
Generate new signed certificate. `ca_key_pair` is the CA key_pair to use Generate new signed certificate. `ca_key_pair` is the CA key_pair to use
if None, use the latest CA key_pair if None, use the latest CA key_pair
...@@ -219,6 +235,8 @@ class CertificateAuthority(object): ...@@ -219,6 +235,8 @@ class CertificateAuthority(object):
new certificate (string). new certificate (string).
@param ca_key_pair: The CA key_pair to used for signature. If None, the @param ca_key_pair: The CA key_pair to used for signature. If None, the
latest key_pair is used. latest key_pair is used.
@param subject_dict: dict of subject attributes to use in x509 subject,
if None, csr subject is used (dict).
""" """
# Apply extensions (ex: "not a certificate", ...) # Apply extensions (ex: "not a certificate", ...)
# Generate a certificate from the CSR # Generate a certificate from the CSR
...@@ -229,9 +247,22 @@ class CertificateAuthority(object): ...@@ -229,9 +247,22 @@ class CertificateAuthority(object):
# Certificate serial is the csr_id without extension .csr.pem # Certificate serial is the csr_id without extension .csr.pem
serial = int(csr_id[:-8], 16) serial = int(csr_id[:-8], 16)
subject = None
if ca_key_pair is None: if ca_key_pair is None:
ca_key_pair = self._ca_key_pairs_list[-1] ca_key_pair = self._ca_key_pairs_list[-1]
cert_pem = self._generateCertificateObjects(ca_key_pair, csr_pem, serial) if subject_dict:
for attr in subject_dict.keys():
if not attr in SUBJECT_KEY_LIST:
  • Why do we even have to check these ? Doesn't pyopenssl (or openssl) reject invalid subject entries (unknown names and malformed values) ?

    Edited by Vincent Pelletier
  • Yes, PyOpenssl will not accept if subject is invalid. But It was a way to catch the problem before go deeper and raise with a clever message.

  • I prefer to not duplicate checks. If the original error is obscure, we cannot do much about it: there is no guarantee our extra checks will catch all cases, so the obscure error may still appear. So I prefer to drop such code and either forward users complaints about unhelpful error messages to pyopenssl/openssl or have a better error translator from pyopenssl exceptions to string errors suitable for REST+json API, as I think it belongs to web API layer (common to web.py and wsgi.py), not to ca.py.

Please register or sign in to reply
raise ValueError("Subject key %r is not allowed. Certificate subject " \
"key should be one of %r" % (attr, SUBJECT_KEY_LIST))
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.")
subject = x509_name(**subject_dict)
cert_pem = self._generateCertificateObjects(ca_key_pair,
csr_pem,
serial,
subject=subject)
crt_id = self._storage.storeCertificate(csr_id, cert_pem) crt_id = self._storage.storeCertificate(csr_id, cert_pem)
return crt_id return crt_id
...@@ -442,12 +473,21 @@ class CertificateAuthority(object): ...@@ -442,12 +473,21 @@ class CertificateAuthority(object):
""" """
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey_object) return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey_object)
def _generateCertificateObjects(self, ca_key_pair, req, serial): def _generateCertificateObjects(self, ca_key_pair, req, serial, subject=None):
""" """
Generate certificate from CSR PEM Object. Generate certificate from CSR PEM Object.
This method set default certificate extensions, later will allow to set custom extensions 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
if subject is None, req.get_subject() is used.
""" """
if subject is None:
subject = req.get_subject()
# Here comes the actual certificate # Here comes the actual certificate
cert = crypto.X509() cert = crypto.X509()
# version v3 # version v3
...@@ -456,7 +496,7 @@ class CertificateAuthority(object): ...@@ -456,7 +496,7 @@ class CertificateAuthority(object):
cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(self.crt_life_time) cert.gmtime_adj_notAfter(self.crt_life_time)
cert.set_issuer(ca_key_pair['crt'].get_subject()) cert.set_issuer(ca_key_pair['crt'].get_subject())
cert.set_subject(req.get_subject()) cert.set_subject(subject)
cert.set_pubkey(req.get_pubkey()) cert.set_pubkey(req.get_pubkey())
self.extension_manager.setDefaultExtensions( self.extension_manager.setDefaultExtensions(
cert, cert,
......
...@@ -66,7 +66,7 @@ class CertificateAuthorityTest(unittest.TestCase): ...@@ -66,7 +66,7 @@ class CertificateAuthorityTest(unittest.TestCase):
auto_sign_csr=auto_sign_csr auto_sign_csr=auto_sign_csr
) )
def generateCSR(self, cn="toto.example.com"): def generateCSR(self, cn="toto.example.com", email="toto@example.com"):
key = crypto.PKey() key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048) key.generate_key(crypto.TYPE_RSA, 2048)
...@@ -78,7 +78,7 @@ class CertificateAuthorityTest(unittest.TestCase): ...@@ -78,7 +78,7 @@ class CertificateAuthorityTest(unittest.TestCase):
subject.L = "LOU" subject.L = "LOU"
subject.O = "OOU" subject.O = "OOU"
subject.OU = "OU" subject.OU = "OU"
subject.emailAddress = "toto@example.com" subject.emailAddress = email
req.set_pubkey(key) req.set_pubkey(key)
utils.X509Extension().setDefaultCsrExtensions(req) utils.X509Extension().setDefaultCsrExtensions(req)
req.sign(key, self.default_digest) req.sign(key, self.default_digest)
...@@ -226,6 +226,63 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow== ...@@ -226,6 +226,63 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow==
cert = ca.getCertificate(cert_id) cert = ca.getCertificate(cert_id)
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.assertTrue(utils.validateCertAndKey(x509, key)) 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',
'ST': 'State',
'L': 'Localisation',
'O': 'My Organisation',
'OU': 'Organisation U',
'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): with self.assertRaises(NotFound):
ca.getPendingCertificateRequest(csr_id) ca.getPendingCertificateRequest(csr_id)
...@@ -482,8 +539,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow== ...@@ -482,8 +539,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow==
crl2_string = ca.getCertificateRevocationList() crl2_string = ca.getCertificateRevocationList()
crl2 = crypto.load_crl(crypto.FILETYPE_PEM, crl2_string) crl2 = crypto.load_crl(crypto.FILETYPE_PEM, crl2_string)
self.assertEquals(len(crl2.get_revoked()), 1) self.assertEquals(len(crl2.get_revoked()), 1)
serial = '0%s' % cert_2.get_serial_number() serial = utils.getSerialToInt(cert_2)
self.assertEquals(crl2.get_revoked()[0].get_serial(), serial) self.assertEquals(crl2.get_revoked()[0].get_serial(), serial.upper())
payload = dict( payload = dict(
reason="", reason="",
...@@ -498,9 +555,9 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow== ...@@ -498,9 +555,9 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow==
self.assertEquals(len(crl3.get_revoked()), 2) self.assertEquals(len(crl3.get_revoked()), 2)
matches = 0 matches = 0
for revoked in crl3.get_revoked(): for revoked in crl3.get_revoked():
if revoked.get_serial() == '0%s' % cert_3.get_serial_number(): if revoked.get_serial() == utils.getSerialToInt(cert_3).upper():
matches += 1 matches += 1
elif revoked.get_serial() == '0%s' % cert_2.get_serial_number(): elif revoked.get_serial() == utils.getSerialToInt(cert_2).upper():
matches += 1 matches += 1
self.assertEquals(matches, 2) self.assertEquals(matches, 2)
...@@ -539,8 +596,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow== ...@@ -539,8 +596,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow==
crl2_string = ca.getCertificateRevocationList() crl2_string = ca.getCertificateRevocationList()
crl2 = crypto.load_crl(crypto.FILETYPE_PEM, crl2_string) crl2 = crypto.load_crl(crypto.FILETYPE_PEM, crl2_string)
self.assertEquals(len(crl2.get_revoked()), 1) self.assertEquals(len(crl2.get_revoked()), 1)
serial = '0%s' % cert_2.get_serial_number() serial = utils.getSerialToInt(cert_2)
self.assertEquals(crl2.get_revoked()[0].get_serial(), serial) self.assertEquals(crl2.get_revoked()[0].get_serial(), serial.upper())
# wait until cert_2 expire # wait until cert_2 expire
time.sleep(3) time.sleep(3)
...@@ -561,8 +618,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow== ...@@ -561,8 +618,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow==
# cert_2 is not longer into crl (expired) # cert_2 is not longer into crl (expired)
self.assertEquals(len(crl3.get_revoked()), 1) self.assertEquals(len(crl3.get_revoked()), 1)
serial = '0%s' % cert_3.get_serial_number() serial = utils.getSerialToInt(cert_3)
self.assertEquals(crl3.get_revoked()[0].get_serial(), serial) self.assertEquals(crl3.get_revoked()[0].get_serial(), serial.upper())
def test_getCertificateRevocationList_with_validation(self): def test_getCertificateRevocationList_with_validation(self):
ca = self.make_ca(158) ca = self.make_ca(158)
...@@ -592,8 +649,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow== ...@@ -592,8 +649,8 @@ m4DpuP4nL0ixQJWZuV+qrx6Tow==
crl_string = ca.getCertificateRevocationList() crl_string = ca.getCertificateRevocationList()
crl = crypto.load_crl(crypto.FILETYPE_PEM, crl_string) crl = crypto.load_crl(crypto.FILETYPE_PEM, crl_string)
self.assertEquals(len(crl.get_revoked()), 1) self.assertEquals(len(crl.get_revoked()), 1)
serial = '0%s' % cert_2.get_serial_number() serial = utils.getSerialToInt(cert_2)
self.assertEquals(crl.get_revoked()[0].get_serial(), serial) self.assertEquals(crl.get_revoked()[0].get_serial(), serial.upper())
with self.assertRaises(CertificateVerificationError): with self.assertRaises(CertificateVerificationError):
utils.verifyCertificateChain(cert_2, utils.verifyCertificateChain(cert_2,
......
...@@ -29,6 +29,7 @@ from OpenSSL import crypto, SSL ...@@ -29,6 +29,7 @@ from OpenSSL import crypto, SSL
from caucase.exceptions import (NoStorage, NotFound, Found) from caucase.exceptions import (NoStorage, NotFound, Found)
from sqlite3 import IntegrityError from sqlite3 import IntegrityError
from caucase import utils from caucase import utils
import uuid
class StorageTest(unittest.TestCase): class StorageTest(unittest.TestCase):
...@@ -113,7 +114,7 @@ class StorageTest(unittest.TestCase): ...@@ -113,7 +114,7 @@ class StorageTest(unittest.TestCase):
return (req, key) return (req, key)
def createCertificate(self, ca_key_pair, req, expire_sec=180): def createCertificate(self, ca_key_pair, req, expire_sec=180):
serial = self._storage.getNextCertificateSerialNumber() serial = uuid.uuid1().int
cert = crypto.X509() cert = crypto.X509()
# 3 = v3 # 3 = v3
cert.set_version(3) cert.set_version(3)
...@@ -301,10 +302,6 @@ class StorageTest(unittest.TestCase): ...@@ -301,10 +302,6 @@ class StorageTest(unittest.TestCase):
# there is only on csr in the list # there is only on csr in the list
self.assertEquals(len(csr_list), 1) self.assertEquals(len(csr_list), 1)
def test_getNextCertificateSerialNumber_empty(self):
serial = self._storage.getNextCertificateSerialNumber()
self.assertEquals(serial, 1)
def test_storeCertificate(self): def test_storeCertificate(self):
keypair = self.createCAKeyPair() keypair = self.createCAKeyPair()
self._storage.storeCAKeyPair(keypair) self._storage.storeCAKeyPair(keypair)
......
...@@ -468,10 +468,10 @@ def get_cacert_json(): ...@@ -468,10 +468,10 @@ def get_cacert_json():
return jsonify(ca_chain_list) return jsonify(ca_chain_list)
def signcert(csr_key, redirect_to=''): def signcert(csr_key, subject_dict=None, redirect_to=''):
try: try:
cert_id = app.config.ca.createCertificate(csr_key) cert_id = app.config.ca.createCertificate(csr_key, subject_dict=subject_dict)
except NotFound, e: except NotFound, e:
raise FlaskException("%s" % str(e), raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1}) status_code=404, payload={"name": "FileNotFound", "code": 1})
...@@ -501,7 +501,15 @@ def sign_cert(): ...@@ -501,7 +501,15 @@ def sign_cert():
raise FlaskException("'csr_id' parameter is a mandatory parameter", raise FlaskException("'csr_id' parameter is a mandatory parameter",
payload={"name": "MissingParameter", "code": 2}) payload={"name": "MissingParameter", "code": 2})
return signcert(key) 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:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
@app.route('/crt/renew', methods=['PUT']) @app.route('/crt/renew', methods=['PUT'])
def renew_cert(): def renew_cert():
...@@ -693,7 +701,11 @@ def do_signcert_web(): ...@@ -693,7 +701,11 @@ def do_signcert_web():
if not csr_id: if not csr_id:
raise FlaskException("'csr_id' parameter is a mandatory parameter", raise FlaskException("'csr_id' parameter is a mandatory parameter",
payload={"name": "MissingParameter", "code": 2}) payload={"name": "MissingParameter", "code": 2})
return signcert(csr_id, 'manage_csr') 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']) @app.route('/admin/deletecsr', methods=['GET'])
@login_required @login_required
......
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