mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 09:29:13 +00:00 
			
		
		
		
	Migrate authority setup from PyOpenSSL to cryptography.io
This commit is contained in:
		
							
								
								
									
										162
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						
									
										162
									
								
								certidude/cli.py
									
									
									
									
									
								
							| @@ -17,25 +17,15 @@ from configparser import ConfigParser | |||||||
| from certidude import constants | from certidude import constants | ||||||
| from certidude.helpers import certidude_request_certificate | from certidude.helpers import certidude_request_certificate | ||||||
| from certidude.common import expand_paths, ip_address, ip_network | from certidude.common import expand_paths, ip_address, ip_network | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from humanize import naturaltime | from humanize import naturaltime | ||||||
| from jinja2 import Environment, PackageLoader | from jinja2 import Environment, PackageLoader | ||||||
| from time import sleep | from time import sleep | ||||||
| from setproctitle import setproctitle | from setproctitle import setproctitle | ||||||
| from OpenSSL import crypto |  | ||||||
|  |  | ||||||
| env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) | env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) | ||||||
|  |  | ||||||
| # Big fat warning: |  | ||||||
| # m2crypto overflows around 2030 because on 32-bit systems |  | ||||||
| # m2crypto does not support hardware engine support (?) |  | ||||||
| # m2crypto CRL object is pretty much useless |  | ||||||
|  |  | ||||||
| # pyopenssl has no straight-forward methods for getting RSA key modulus |  | ||||||
|  |  | ||||||
| # pyopenssl 0.13 bundled with Ubuntu 14.04 has no get_extension_count() for X509Req objects |  | ||||||
| assert hasattr(crypto.X509Req(), "get_extensions"), "You're running too old version of pyopenssl, upgrade to 0.15+" |  | ||||||
|  |  | ||||||
| # http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml | # http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml | ||||||
| # https://kjur.github.io/jsrsasign/ | # https://kjur.github.io/jsrsasign/ | ||||||
| # keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_config.html | # keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_config.html | ||||||
| @@ -778,16 +768,20 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw | |||||||
| @click.option("--revocation-list-lifetime", default=20*60, help="Revocation list lifetime in days, 1200 seconds (20 minutes) by default") | @click.option("--revocation-list-lifetime", default=20*60, help="Revocation list lifetime in days, 1200 seconds (20 minutes) by default") | ||||||
| @click.option("--organization", "-o", default=None, help="Company or organization name") | @click.option("--organization", "-o", default=None, help="Company or organization name") | ||||||
| @click.option("--organizational-unit", "-ou", default=None) | @click.option("--organizational-unit", "-ou", default=None) | ||||||
| @click.option("--pkcs11", default=False, is_flag=True, help="Use PKCS#11 token instead of files") |  | ||||||
| @click.option("--revoked-url", default=None, help="CRL distribution URL") | @click.option("--revoked-url", default=None, help="CRL distribution URL") | ||||||
| @click.option("--certificate-url", default=None, help="Authority certificate URL") | @click.option("--certificate-url", default=None, help="Authority certificate URL") | ||||||
| @click.option("--ocsp-responder-url", default=None, help="OCSP responder URL") |  | ||||||
| @click.option("--push-server", default="http://push.%s" % constants.DOMAIN, help="Push server, http://push.%s by default" % constants.DOMAIN) | @click.option("--push-server", default="http://push.%s" % constants.DOMAIN, help="Push server, http://push.%s by default" % constants.DOMAIN) | ||||||
| @click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA") | @click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA") | ||||||
| @click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/%s/ by default" % FQDN) | @click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/%s/ by default" % FQDN) | ||||||
| @click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags") | @click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags") | ||||||
| @click.option("--outbox", default="smtp://smtp.%s" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN) | @click.option("--outbox", default="smtp://smtp.%s" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN) | ||||||
| def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, revoked_url, certificate_url, ocsp_responder_url, push_server, email_address, outbox, server_flags): | def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, email_address, outbox, server_flags): | ||||||
|  |  | ||||||
|  |     from cryptography import x509 | ||||||
|  |     from cryptography.x509.oid import NameOID | ||||||
|  |     from cryptography.hazmat.backends import default_backend | ||||||
|  |     from cryptography.hazmat.primitives import hashes, serialization | ||||||
|  |     from cryptography.hazmat.primitives.asymmetric import rsa | ||||||
|  |  | ||||||
|     # Make sure common_name is valid |     # Make sure common_name is valid | ||||||
|     if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name): |     if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name): | ||||||
| @@ -804,11 +798,11 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|  |  | ||||||
|     click.echo("Generating 4096-bit RSA key...") |     click.echo("Generating 4096-bit RSA key...") | ||||||
|  |  | ||||||
|     if pkcs11: |     key = rsa.generate_private_key( | ||||||
|         raise NotImplementedError("Hardware token support not yet implemented!") |         public_exponent=65537, | ||||||
|     else: |         key_size=4096, | ||||||
|         key = crypto.PKey() |         backend=default_backend() | ||||||
|         key.generate_key(crypto.TYPE_RSA, 4096) |     ) | ||||||
|  |  | ||||||
|     if not revoked_url: |     if not revoked_url: | ||||||
|         revoked_url = "http://%s/api/revoked/" % common_name |         revoked_url = "http://%s/api/revoked/" % common_name | ||||||
| @@ -819,85 +813,59 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|     ca_key = os.path.join(directory, "ca_key.pem") |     ca_key = os.path.join(directory, "ca_key.pem") | ||||||
|     ca_crt = os.path.join(directory, "ca_crt.pem") |     ca_crt = os.path.join(directory, "ca_crt.pem") | ||||||
|  |  | ||||||
|     ca = crypto.X509() |  | ||||||
|     ca.set_version(2) # This corresponds to X.509v3 |  | ||||||
|     ca.set_serial_number(1) |  | ||||||
|     ca.get_subject().CN = common_name |  | ||||||
|  |  | ||||||
|     if country: |     subject = issuer = x509.Name([ | ||||||
|         ca.get_subject().C = country |         x509.NameAttribute(o, value) for o, value in ( | ||||||
|     if state: |             (NameOID.COUNTRY_NAME, country), | ||||||
|         ca.get_subject().ST = state |             (NameOID.STATE_OR_PROVINCE_NAME, state), | ||||||
|     if locality: |             (NameOID.LOCALITY_NAME, locality), | ||||||
|         ca.get_subject().L = locality |             (NameOID.ORGANIZATION_NAME, organization), | ||||||
|     if organization: |             (NameOID.COMMON_NAME, common_name), | ||||||
|         ca.get_subject().O = organization |         ) if value | ||||||
|     if organizational_unit: |  | ||||||
|         ca.get_subject().OU = organizational_unit |  | ||||||
|  |  | ||||||
|     ca.gmtime_adj_notBefore(0) |  | ||||||
|     ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) |  | ||||||
|     ca.set_issuer(ca.get_subject()) |  | ||||||
|     ca.set_pubkey(key) |  | ||||||
|  |  | ||||||
|     # add_extensions shall be called only once and |  | ||||||
|     # there has to be only one subjectAltName! |  | ||||||
|     ca.add_extensions([ |  | ||||||
|         crypto.X509Extension( |  | ||||||
|             b"basicConstraints", |  | ||||||
|             True, |  | ||||||
|             b"CA:TRUE"), |  | ||||||
|         crypto.X509Extension( |  | ||||||
|             b"keyUsage", |  | ||||||
|             True, |  | ||||||
|             b"digitalSignature, keyCertSign, cRLSign"), |  | ||||||
|         crypto.X509Extension( |  | ||||||
|             b"subjectKeyIdentifier", |  | ||||||
|             False, |  | ||||||
|             b"hash", |  | ||||||
|             subject = ca), |  | ||||||
|         crypto.X509Extension( |  | ||||||
|             b"subjectAltName", |  | ||||||
|             False, |  | ||||||
|             (u"DNS: %s, email: %s" % (common_name, email_address)).encode("ascii")) |  | ||||||
|     ]) |     ]) | ||||||
|  |  | ||||||
|  |     builder = x509.CertificateBuilder( | ||||||
|  |         ).subject_name(subject | ||||||
|  |         ).issuer_name(issuer | ||||||
|  |         ).public_key(key.public_key() | ||||||
|  |         ).not_valid_before(datetime.utcnow() | ||||||
|  |         ).not_valid_after( | ||||||
|  |             datetime.utcnow() + timedelta(days=authority_lifetime) | ||||||
|  |         ).serial_number(1 | ||||||
|  |         ).add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True, | ||||||
|  |         ).add_extension(x509.KeyUsage( | ||||||
|  |             digital_signature=True, | ||||||
|  |             key_encipherment=False, | ||||||
|  |             content_commitment=False, | ||||||
|  |             data_encipherment=False, | ||||||
|  |             key_agreement=False, | ||||||
|  |             key_cert_sign=True, | ||||||
|  |             crl_sign=True, | ||||||
|  |             encipher_only=False, | ||||||
|  |             decipher_only=False), critical=True, | ||||||
|  |         ).add_extension( | ||||||
|  |             x509.SubjectAlternativeName([ | ||||||
|  |                 x509.DNSName(common_name), | ||||||
|  |                 x509.RFC822Name(email_address) | ||||||
|  |             ]), | ||||||
|  |             critical=False, | ||||||
|  |         ).add_extension( | ||||||
|  |             x509.SubjectKeyIdentifier.from_public_key(key.public_key()), | ||||||
|  |             critical=False | ||||||
|  |         ).add_extension( | ||||||
|  |             x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()), | ||||||
|  |             critical=False | ||||||
|  |  | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     if server_flags: |     if server_flags: | ||||||
|         ca.add_extensions([ |         builder = builder.add_extension(x509.ExtendedKeyUsage([ | ||||||
|             crypto.X509Extension( |             ExtendedKeyUsageOID.CLIENT_AUTH, | ||||||
|                 b"extendedKeyUsage", |             ObjectIdentifier("1.3.6.1.5.5.8.2.2")])) | ||||||
|                 False, |  | ||||||
|                 b"serverAuth,1.3.6.1.5.5.8.2.2") |  | ||||||
|         ]) |  | ||||||
|  |  | ||||||
|     ca.add_extensions([ |     cert = builder.sign(key, hashes.SHA512(), default_backend()) | ||||||
|         crypto.X509Extension( |  | ||||||
|             b"authorityKeyIdentifier", |  | ||||||
|             False, |  | ||||||
|             b"keyid:always", |  | ||||||
|             issuer = ca) |  | ||||||
|     ]) |  | ||||||
|  |  | ||||||
|     if ocsp_responder_url: |     click.echo("Signing %s..." % cert.subject) | ||||||
|         raise NotImplementedError() |  | ||||||
|  |  | ||||||
|     """ |  | ||||||
|         ocsp_responder_url = "http://%s/api/ocsp/" % common_name |  | ||||||
|         authority_info_access = "OCSP;URI:%s" % ocsp_responder_url |  | ||||||
|         ca.add_extensions([ |  | ||||||
|             crypto.X509Extension( |  | ||||||
|                 b"authorityInfoAccess", |  | ||||||
|                 False, |  | ||||||
|                 authority_info_access.encode("ascii")) |  | ||||||
|         ]) |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     click.echo("Signing %s..." % ca.get_subject()) |  | ||||||
|  |  | ||||||
|     # openssl x509 -in ca_crt.pem -outform DER | sha256sum |  | ||||||
|     # openssl x509 -fingerprint -in ca_crt.pem |  | ||||||
|  |  | ||||||
|     ca.sign(key, "sha512") |  | ||||||
|  |  | ||||||
|     _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") |     _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") | ||||||
|     os.setgid(gid) |     os.setgid(gid) | ||||||
| @@ -918,12 +886,16 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | |||||||
|     with open(certidude_conf, "w") as fh: |     with open(certidude_conf, "w") as fh: | ||||||
|         fh.write(env.get_template("certidude.conf").render(vars())) |         fh.write(env.get_template("certidude.conf").render(vars())) | ||||||
|     with open(ca_crt, "wb") as fh: |     with open(ca_crt, "wb") as fh: | ||||||
|         fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca)) |         fh.write(cert.public_bytes(serialization.Encoding.PEM)) | ||||||
|  |  | ||||||
|     # Set permission bits to 600 |     # Set permission bits to 600 | ||||||
|     os.umask(0o177) |     os.umask(0o177) | ||||||
|     with open(ca_key, "wb") as fh: |     with open(ca_key, "wb") as fh: | ||||||
|         fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) |         fh.write(key.private_bytes( | ||||||
|  |             encoding=serialization.Encoding.PEM, | ||||||
|  |             format=serialization.PrivateFormat.TraditionalOpenSSL, | ||||||
|  |             encryption_algorithm=serialization.NoEncryption() # TODO: Implement passphrase | ||||||
|  |         )) | ||||||
|  |  | ||||||
|     click.echo() |     click.echo() | ||||||
|     click.echo("Use following commands to inspect the newly created files:") |     click.echo("Use following commands to inspect the newly created files:") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user