From aecd886b7a1268c52e8915c27db6966952645314 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 6 Feb 2024 20:51:28 +0000 Subject: [PATCH 01/31] At least allow things to import properly --- pyproject.toml | 4 ++-- requirements.txt | 4 ++-- synapse/lib/crypto/rsa.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1aad947f1b..60f00ed0a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,8 @@ classifiers = [ 'Programming Language :: Python :: 3.11', ] dependencies = [ - 'pyOpenSSL>=23.0.0,<23.3.0', - 'cryptography>=39.0.1,<42.0.0', + 'pyOpenSSL>=24.0.0,<25.3.0', + 'cryptography>=42.0.0,<43.0.0', 'msgpack>=1.0.5,<1.1.0', 'xxhash>=1.4.4,<3.5.0', 'lmdb>=1.2.1,<1.5.0', diff --git a/requirements.txt b/requirements.txt index c06fb8179b..f735ab6704 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyOpenSSL>=23.0.0,<23.3.0 +pyOpenSSL>=24.0.0,<25.3.0 msgpack>=1.0.5,<1.1.0 xxhash>=1.4.4,<3.5.0 lmdb>=1.2.1,<1.5.0 @@ -32,4 +32,4 @@ beautifulsoup4[html5lib]>=4.11.1,<5.0.0 # pin. Cryptography also vendors a copy of OpenSSL, so it needs to be able to # have a minimum version bumped in the event of a OpenSSL vulnerability that # needs to be patched. -cryptography>=39.0.1,<42.0.0 +cryptography>=42.0.0,<43.0.0 diff --git a/synapse/lib/crypto/rsa.py b/synapse/lib/crypto/rsa.py index a14810400b..1d82f1a390 100644 --- a/synapse/lib/crypto/rsa.py +++ b/synapse/lib/crypto/rsa.py @@ -2,9 +2,8 @@ import cryptography.hazmat.primitives.hashes as c_hashes import cryptography.hazmat.primitives.serialization as c_ser -import cryptography.hazmat.primitives.asymmetric.utils as c_utils +import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa import cryptography.hazmat.primitives.asymmetric.padding as c_padding -import cryptography.hazmat.backends.openssl.rsa as c_rsa import synapse.exc as s_exc import synapse.common as s_common From b4b8244f3d51e5b3f3c84e80e43da667b9cd36bb Mon Sep 17 00:00:00 2001 From: epiphyte Date: Fri, 9 Feb 2024 15:38:48 +0000 Subject: [PATCH 02/31] Steel thread for a certdir rewrite - CA generation is working. --- synapse/lib/certdir.py | 1386 ++++++++++++++++++++++++++++- synapse/tests/test_lib_certdir.py | 48 + 2 files changed, 1433 insertions(+), 1 deletion(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 6b40e6ac47..460882e64d 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -5,14 +5,24 @@ import shutil import socket import logging +import datetime import collections +from typing import AnyStr, ByteString, Optional, Tuple, Union + from OpenSSL import crypto # type: ignore import synapse.exc as s_exc import synapse.common as s_common +import synapse.lib.output as s_output import synapse.lib.crypto.rsa as s_rsa +import cryptography.x509 as c_x509 +import cryptography.hazmat.primitives.hashes as c_hashes +import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa +import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa +import cryptography.hazmat.primitives.serialization as c_serialization + defdir_default = '~/.syn/certs' defdir = os.getenv('SYN_CERT_DIR') if defdir is None: @@ -20,8 +30,16 @@ logger = logging.getLogger(__name__) -TEN_YEARS = 10 * 365 * 24 * 60 * 60 +TEN_YEARS = 10 * 365 * 24 * 60 * 60 # 10 years in seconds +TEN_YEARS_TD = datetime.timedelta(seconds=TEN_YEARS) +StrOrNoneType = Union[AnyStr | None] +BytesOrNoneType = Union[ByteString | None] +CertOrNoneType = Union[c_x509.Certificate | None] +PkeyOrNoneType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey | None] +PkeyAndCertType = Tuple[c_rsa.RSAPrivateKey, c_x509.Certificate] +PkeyAndBuilderType = Tuple[c_rsa.RSAPrivateKey, c_x509.CertificateBuilder] +PkeyType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey] def iterFqdnUp(fqdn): levs = fqdn.split('.') @@ -1489,3 +1507,1369 @@ def getCertDirn() -> str: str: The path string. ''' return s_common.genpath(defdir) + +class CertDirNew: + ''' + Certificate loading/generation/signing utilities. + + Features: + * Locates and load certificates, keys, and certificate signing requests (CSRs). + * Generates keypairs for users, hosts, and certificate authorities (CAs), supports both signed and self-signed. + * Generates certificate signing requests (CSRs) for users, hosts, and certificate authorities (CAs). + * Signs certificate signing requests (CSRs). + * Generates PKCS#12 archives for use in browser. + + Args: + path (str): Optional path which can override the default path directory. + + Notes: + * All certificates will be loaded from and written to ~/.syn/certs by default. Set the environment variable + SYN_CERT_DIR to override. + * All certificate generation methods create 4096 bit RSA keypairs. + * All certificate signing methods use sha256 as the signature algorithm. + * CertDir does not currently support signing CA CSRs. + ''' + + def __init__(self, path=None): + self.crypto_numbits = 4096 + self.signing_digest = c_hashes.SHA256 + + self.certdirs = [] + self.pathrefs = collections.defaultdict(int) + + if path is None: + path = (defdir,) + + if not isinstance(path, (list, tuple)): + path = (path,) + + for p in path: + self.addCertPath(p) + + def addCertPath(self, *path): + + fullpath = s_common.genpath(*path) + self.pathrefs[fullpath] += 1 + + if self.pathrefs[fullpath] == 1: + self.certdirs.append(fullpath) + + def delCertPath(self, *path): + fullpath = s_common.genpath(*path) + self.pathrefs[fullpath] -= 1 + if self.pathrefs[fullpath] <= 0: + self.certdirs.remove(fullpath) + self.pathrefs.pop(fullpath, None) + + def genCaCert(self, name: AnyStr, + signas: Optional[AnyStr | None] = None, + outp: s_output.OutPut =None, + save: bool =True) -> PkeyAndCertType: + ''' + Generates a CA keypair. + + Args: + name (str): The name of the CA keypair. + signas (str): The CA keypair to sign the new CA with. + outp (synapse.lib.output.OutPut): The output buffer. + save (bool): + + Examples: + Make a CA named "myca": + + mycakey, mycacert = cdir.genCaCert('myca') + + Returns: + #XXX FIX StrOrNoneType: + ''' + Gets the path to a CA certificate. + + Args: + name (str): The name of the CA keypair. + + Examples: + Get the path to the CA certificate for the CA "myca": + + mypath = cdir.getCACertPath('myca') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'cas', '%s.crt' % name) + if os.path.isfile(path): + return path + + def getCaKey(self, name) -> PkeyOrNoneType: + ''' + Loads the PKey object for a given CA keypair. + + Args: + name (str): The name of the CA keypair. + + Examples: + Get the private key for the CA "myca": + + mycakey = cdir.getCaKey('myca') + + Returns: + # XXX FIXME Correct the type + : The private key, if exists. + ''' + return self._loadKeyPath(self.getCaKeyPath(name)) + + def getCaKeyPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to a CA key. + + Args: + name (str): The name of the CA keypair. + + Examples: + Get the path to the private key for the CA "myca": + + mypath = cdir.getCAKeyPath('myca') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'cas', '%s.key' % name) + if os.path.isfile(path): + return path + + # def getClientCert(self, name): + # ''' + # Loads the PKCS12 archive object for a given user keypair. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Get the PKCS12 object for the user "myuser": + # + # mypkcs12 = cdir.getClientCert('myuser') + # + # Notes: + # The PKCS12 archive will contain private key material if it was created with CertDir or the easycert tool + # + # Returns: + # OpenSSL.crypto.PKCS12: The PKCS12 archive, if exists. + # ''' + # return self._loadP12Path(self.getClientCertPath(name)) + # + # def getClientCertPath(self, name): + # ''' + # Gets the path to a client certificate. + # + # Args: + # name (str): The name of the client keypair. + # + # Examples: + # Get the path to the client certificate for "myuser": + # + # mypath = cdir.getClientCertPath('myuser') + # + # Returns: + # str: The path if exists. + # ''' + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'users', '%s.p12' % name) + # if os.path.isfile(path): + # return path + # + # def getHostCaPath(self, name): + # ''' + # Gets the path to the CA certificate that issued a given host keypair. + # + # Args: + # name (str): The name of the host keypair. + # + # Examples: + # Get the path to the CA cert which issue the cert for "myhost": + # + # mypath = cdir.getHostCaPath('myhost') + # + # Returns: + # str: The path if exists. + # ''' + # cert = self.getHostCert(name) + # if cert is None: + # return None + # + # return self._getCaPath(cert) + # + # def getHostCert(self, name): + # ''' + # Loads the X509 object for a given host keypair. + # + # Args: + # name (str): The name of the host keypair. + # + # Examples: + # Get the certificate object for the host "myhost": + # + # myhostcert = cdir.getHostCert('myhost') + # + # Returns: + # OpenSSL.crypto.X509: The certificate, if exists. + # ''' + # return self._loadCertPath(self.getHostCertPath(name)) + # + # def getHostCertHash(self, name): + # cert = self.getHostCert(name) + # if cert is None: + # return None + # return cert.digest('SHA256').decode().lower().replace(':', '') + # + # def getHostCertPath(self, name): + # ''' + # Gets the path to a host certificate. + # + # Args: + # name (str): The name of the host keypair. + # + # Examples: + # Get the path to the host certificate for the host "myhost": + # + # mypath = cdir.getHostCertPath('myhost') + # + # Returns: + # str: The path if exists. + # ''' + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'hosts', '%s.crt' % name) + # if os.path.isfile(path): + # return path + # + # def getHostKey(self, name): + # ''' + # Loads the PKey object for a given host keypair. + # + # Args: + # name (str): The name of the host keypair. + # + # Examples: + # Get the private key object for the host "myhost": + # + # myhostkey = cdir.getHostKey('myhost') + # + # Returns: + # OpenSSL.crypto.PKey: The private key, if exists. + # ''' + # return self._loadKeyPath(self.getHostKeyPath(name)) + # + # def getHostKeyPath(self, name): + # ''' + # Gets the path to a host key. + # + # Args: + # name (str): The name of the host keypair. + # + # Examples: + # Get the path to the host key for the host "myhost": + # + # mypath = cdir.getHostKeyPath('myhost') + # + # Returns: + # str: The path if exists. + # ''' + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'hosts', '%s.key' % name) + # if os.path.isfile(path): + # return path + # + # def getUserCaPath(self, name): + # ''' + # Gets the path to the CA certificate that issued a given user keypair. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Get the path to the CA cert which issue the cert for "myuser": + # + # mypath = cdir.getUserCaPath('myuser') + # + # Returns: + # str: The path if exists. + # ''' + # cert = self.getUserCert(name) + # if cert is None: + # return None + # + # return self._getCaPath(cert) + # + # def getUserCert(self, name): + # ''' + # Loads the X509 object for a given user keypair. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Get the certificate object for the user "myuser": + # + # myusercert = cdir.getUserCert('myuser') + # + # Returns: + # OpenSSL.crypto.X509: The certificate, if exists. + # ''' + # return self._loadCertPath(self.getUserCertPath(name)) + # + # def getUserCertPath(self, name): + # ''' + # Gets the path to a user certificate. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Get the path for the user cert for "myuser": + # + # mypath = cdir.getUserCertPath('myuser') + # + # Returns: + # str: The path if exists. + # ''' + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'users', '%s.crt' % name) + # if os.path.isfile(path): + # return path + # + # def getUserForHost(self, user, host): + # ''' + # Gets the name of the first existing user cert for a given user and host. + # + # Args: + # user (str): The name of the user. + # host (str): The name of the host. + # + # Examples: + # Get the name for the "myuser" user cert at "cool.vertex.link": + # + # usercertname = cdir.getUserForHost('myuser', 'cool.vertex.link') + # + # Returns: + # str: The cert name, if exists. + # ''' + # for name in iterFqdnUp(host): + # usercert = '%s@%s' % (user, name) + # if self.isUserCert(usercert): + # return usercert + # + # def getUserKey(self, name): + # ''' + # Loads the PKey object for a given user keypair. + # + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Get the key object for the user key for "myuser": + # + # myuserkey = cdir.getUserKey('myuser') + # + # Returns: + # OpenSSL.crypto.PKey: The private key, if exists. + # ''' + # return self._loadKeyPath(self.getUserKeyPath(name)) + # + # def getUserKeyPath(self, name): + # ''' + # Gets the path to a user key. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Get the path to the user key for "myuser": + # + # mypath = cdir.getUserKeyPath('myuser') + # + # Returns: + # str: The path if exists. + # ''' + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'users', '%s.key' % name) + # if os.path.isfile(path): + # return path + # + # def getUserCsrPath(self, name): + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'users', '%s.csr' % name) + # if os.path.isfile(path): + # return path + # + # def getHostCsrPath(self, name): + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) + # if os.path.isfile(path): + # return path + # def importFile(self, path, mode, outp=None): + # ''' + # Imports certs and keys into the Synapse cert directory + # + # Args: + # path (str): The path of the file to be imported. + # mode (str): The certdir subdirectory to import the file into. + # + # Examples: + # Import CA certifciate 'mycoolca.crt' to the 'cas' directory. + # + # certdir.importFile('mycoolca.crt', 'cas') + # + # Notes: + # importFile does not perform any validation on the files it imports. + # + # Returns: + # None + # ''' + # if not os.path.isfile(path): + # raise s_exc.NoSuchFile(mesg=f'File {path} does not exist', path=path) + # + # fname = os.path.split(path)[1] + # parts = fname.rsplit('.', 1) + # ext = parts[1] if len(parts) == 2 else None + # + # if not ext or ext not in ('crt', 'key', 'p12'): + # mesg = 'importFile only supports .crt, .key, .p12 extensions' + # raise s_exc.BadFileExt(mesg=mesg, ext=ext) + # + # newpath = s_common.genpath(self.certdirs[0], mode, fname) + # if os.path.isfile(newpath): + # raise s_exc.FileExists(mesg=f'File {newpath} already exists', path=path) + # + # s_common.gendir(os.path.dirname(newpath)) + # + # shutil.copy(path, newpath) + # if outp is not None: + # outp.printf('copied %s to %s' % (path, newpath)) + # + def isCaCert(self, name: AnyStr) -> bool: + ''' + Checks if a CA certificate exists. + + Args: + name (str): The name of the CA keypair. + + Examples: + Check if the CA certificate for "myca" exists: + + exists = cdir.isCaCert('myca') + + Returns: + bool: True if the certificate is present, False otherwise. + ''' + return self.getCaCertPath(name) is not None + # + # def isClientCert(self, name): + # ''' + # Checks if a user client certificate (PKCS12) exists. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Check if the client certificate "myuser" exists: + # + # exists = cdir.isClientCert('myuser') + # + # Returns: + # bool: True if the certificate is present, False otherwise. + # ''' + # crtpath = self._getPathJoin('users', '%s.p12' % name) + # return os.path.isfile(crtpath) + # + # def isHostCert(self, name): + # ''' + # Checks if a host certificate exists. + # + # Args: + # name (str): The name of the host keypair. + # + # Examples: + # Check if the host cert "myhost" exists: + # + # exists = cdir.isUserCert('myhost') + # + # Returns: + # bool: True if the certificate is present, False otherwise. + # ''' + # return self.getHostCertPath(name) is not None + # + # def isUserCert(self, name): + # ''' + # Checks if a user certificate exists. + # + # Args: + # name (str): The name of the user keypair. + # + # Examples: + # Check if the user cert "myuser" exists: + # + # exists = cdir.isUserCert('myuser') + # + # Returns: + # bool: True if the certificate is present, False otherwise. + # ''' + # return self.getUserCertPath(name) is not None + # + # def signCertAs(self, cert, signas): + # ''' + # Signs a certificate with a CA keypair. + # + # Args: + # cert (OpenSSL.crypto.X509): The certificate to sign. + # signas (str): The CA keypair name to sign the new keypair with. + # + # Examples: + # Sign a certificate with the CA "myca": + # + # cdir.signCertAs(mycert, 'myca') + # + # Returns: + # None + # ''' + # cakey = self.getCaKey(signas) + # if cakey is None: + # raise s_exc.NoCertKey(mesg=f'Missing .key for {signas}') + # cacert = self.getCaCert(signas) + # if cacert is None: + # raise s_exc.NoCertKey(mesg=f'Missing .crt for {signas}') + # + # cert.set_issuer(cacert.get_subject()) + # cert.sign(cakey, self.signing_digest) + # + # def signHostCsr(self, xcsr, signas, outp=None, sans=None, save=True): + # ''' + # Signs a host CSR with a CA keypair. + # + # Args: + # xcsr (OpenSSL.crypto.X509Req): The certificate signing request. + # signas (str): The CA keypair name to sign the CSR with. + # outp (synapse.lib.output.Output): The output buffer. + # sans (list): List of subject alternative names. + # + # Examples: + # Sign a host key with the CA "myca": + # + # cdir.signHostCsr(mycsr, 'myca') + # + # Returns: + # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. + # ''' + # pkey = xcsr.get_pubkey() + # name = xcsr.get_subject().CN + # return self.genHostCert(name, csr=pkey, signas=signas, outp=outp, sans=sans, save=save) + # + def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_x509.Certificate: + ''' + Self-sign a certificate. + + Args: + cert (OpenSSL.crypto.X509): The certificate to sign. + pkey (OpenSSL.crypto.PKey): The PKey with which to sign the certificate. + + Examples: + Sign a given certificate with a given private key: + + cdir.selfSignCert(mycert, myotherprivatekey) + + Returns: + None + ''' + attr = builder._subject_name.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value + builder = builder.issuer_name(c_x509.Name([ + c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), + ])) + certificate = builder.sign( + private_key=pkey, algorithm=self.signing_digest(), + ) + return certificate + + # + # def signUserCsr(self, xcsr, signas, outp=None, save=True): + # ''' + # Signs a user CSR with a CA keypair. + # + # Args: + # xcsr (OpenSSL.crypto.X509Req): The certificate signing request. + # signas (str): The CA keypair name to sign the CSR with. + # outp (synapse.lib.output.Output): The output buffer. + # + # Examples: + # cdir.signUserCsr(mycsr, 'myca') + # + # Returns: + # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. + # ''' + # pkey = xcsr.get_pubkey() + # name = xcsr.get_subject().CN + # return self.genUserCert(name, csr=pkey, signas=signas, outp=outp, save=save) + # + # def _loadCasIntoSSLContext(self, ctx): + # + # for cdir in self.certdirs: + # + # path = s_common.genpath(cdir, 'cas') + # if not os.path.isdir(path): + # continue + # + # for name in os.listdir(path): + # if name.endswith('.crt'): + # ctx.load_verify_locations(os.path.join(path, name)) + # + # def getClientSSLContext(self, certname=None): + # ''' + # Returns an ssl.SSLContext appropriate for initiating a TLS session + # + # Args: + # certname: If specified, use the user certificate with the matching + # name to authenticate to the remote service. + # Returns: + # ssl.SSLContext: A SSLContext object. + # ''' + # sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + # sslctx.minimum_version = ssl.TLSVersion.TLSv1_2 + # self._loadCasIntoSSLContext(sslctx) + # + # if certname is not None: + # + # username = certname + # if username.find('@') != -1: + # user, host = username.split('@', 1) + # username = self.getUserForHost(user, host) + # + # if username is None: + # mesg = f'User certificate not found: {certname}' + # raise s_exc.NoSuchCert(mesg=mesg) + # + # certpath = self.getUserCertPath(username) + # if certpath is None: + # mesg = f'User certificate not found: {certname}' + # raise s_exc.NoSuchCert(mesg=mesg) + # + # keypath = self.getUserKeyPath(username) + # if keypath is None: + # mesg = f'User private key not found: {certname}' + # raise s_exc.NoCertKey(mesg=mesg) + # + # sslctx.load_cert_chain(certpath, keypath) + # + # return sslctx + # + # def getServerSSLContext(self, hostname=None, caname=None): + # ''' + # Returns an ssl.SSLContext appropriate to listen on a socket + # + # Args: + # + # hostname: If None, the value from socket.gethostname is used to find the key in the servers directory. + # This name should match the not-suffixed part of two files ending in .key and .crt in the hosts + # subdirectory. + # + # caname: If not None, the given name is used to locate a CA certificate used to validate client SSL certs. + # + # Returns: + # ssl.SSLContext: A SSLContext object. + # ''' + # if hostname is not None and hostname.find(',') != -1: + # # multi-hostname SNI routing has been requested + # ctxs = {} + # names = hostname.split(',') + # for name in names: + # ctxs[name] = self._getServerSSLContext(name, caname=caname) + # + # def snifunc(sslsock, sslname, origctx): + # sslsock.context = ctxs.get(sslname, origctx) + # return None + # + # sslctx = ctxs.get(names[0]) + # sslctx.sni_callback = snifunc + # return sslctx + # + # return self._getServerSSLContext(hostname=hostname, caname=caname) + # + # def getCrlPath(self, name): + # for cdir in self.certdirs: + # path = s_common.genpath(cdir, 'crls', '%s.crl' % name) + # if os.path.isfile(path): + # return path + # + # def genCrlPath(self, name): + # path = self.getCrlPath(name) + # if path is None: + # s_common.gendir(self.certdirs[0], 'crls') + # path = os.path.join(self.certdirs[0], 'crls', f'{name}.crl') + # return path + # + # def genCaCrl(self, name): + # ''' + # Get the CRL for a given CA. + # + # Args: + # name (str): The CA name. + # + # Returns: + # CRL: The CRL object. + # ''' + # return CRL(self, name) + # + # def _getServerSSLContext(self, hostname=None, caname=None): + # sslctx = getServerSSLContext() + # + # if hostname is None: + # hostname = socket.gethostname() + # + # certfile = self.getHostCertPath(hostname) + # if certfile is None: + # mesg = f'Missing TLS certificate file for host: {hostname}' + # raise s_exc.NoCertKey(mesg=mesg) + # + # keyfile = self.getHostKeyPath(hostname) + # if keyfile is None: + # mesg = f'Missing TLS key file for host: {hostname}' + # raise s_exc.NoCertKey(mesg=mesg) + # + # sslctx.load_cert_chain(certfile, keyfile) + # + # if caname is not None: + # cafile = self.getCaCertPath(caname) + # sslctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED + # sslctx.load_verify_locations(cafile=cafile) + # + # return sslctx + # + # def saveCertPem(self, cert, path): + # ''' + # Save a certificate in PEM format to a file outside the certdir. + # ''' + # with s_common.genfile(path) as fd: + # fd.truncate(0) + # fd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + # + # def savePkeyPem(self, pkey, path): + # ''' + # Save a private key in PEM format to a file outside the certdir. + # ''' + # with s_common.genfile(path) as fd: + # fd.truncate(0) + # fd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + # + # def saveCaCertByts(self, byts): + # cert = self._loadCertByts(byts) + # name = cert.get_subject().CN + # return self._saveCertTo(cert, 'cas', f'{name}.crt') + # + # def saveHostCertByts(self, byts): + # cert = self._loadCertByts(byts) + # name = cert.get_subject().CN + # return self._saveCertTo(cert, 'hosts', f'{name}.crt') + # + # def saveUserCertByts(self, byts): + # cert = self._loadCertByts(byts) + # name = cert.get_subject().CN + # return self._saveCertTo(cert, 'users', f'{name}.crt') + # + def _checkDupFile(self, path) -> None: + if os.path.isfile(path): + raise s_exc.DupFileName(mesg=f'Duplicate file {path}', path=path) + + def _genBasePkeyCert(self, name: AnyStr, pkey: PkeyOrNoneType =None) -> PkeyAndBuilderType: + + if pkey is None: + pkey = c_rsa.generate_private_key(65537, self.crypto_numbits) + + builder = c_x509.CertificateBuilder() + builder = builder.subject_name(c_x509.Name([ + c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), + ])) + + now = datetime.datetime.utcnow() + + builder = builder.not_valid_before(datetime.datetime.utcnow()) + builder = builder.not_valid_after(now + TEN_YEARS_TD) # Certpairs are good for 10 years + builder = builder.serial_number(int(s_common.guid(), 16)) + builder = builder.public_key(pkey.public_key()) + return pkey, builder + # + # def _genPkeyCsr(self, name, mode, outp=None): + # pkey = crypto.PKey() + # pkey.generate_key(crypto.TYPE_RSA, self.crypto_numbits) + # + # xcsr = crypto.X509Req() + # xcsr.get_subject().CN = name + # + # xcsr.set_pubkey(pkey) + # xcsr.sign(pkey, self.signing_digest) + # + # keypath = self._savePkeyTo(pkey, mode, '%s.key' % name) + # if outp is not None: + # outp.printf('key saved: %s' % (keypath,)) + # + # csrpath = self._getPathJoin(mode, '%s.csr' % name) + # self._checkDupFile(csrpath) + # + # byts = crypto.dump_certificate_request(crypto.FILETYPE_PEM, xcsr) + # + # with s_common.genfile(csrpath) as fd: + # fd.truncate(0) + # fd.write(byts) + # + # if outp is not None: + # outp.printf('csr saved: %s' % (csrpath,)) + # + # return byts + # + # def _getCaPath(self, cert): + # subj = cert.get_issuer() + # return self.getCaCertPath(subj.CN) + # + def _getPathBytes(self, path: AnyStr) -> BytesOrNoneType: + if path is None: + return None + return s_common.getbytes(path) + # + def _getPathJoin(self, *paths: AnyStr) -> AnyStr: + '''Get the base certidr path + paths''' + return s_common.genpath(self.certdirs[0], *paths) + + def _loadCertPath(self, path: AnyStr) -> CertOrNoneType: + byts = self._getPathBytes(path) + if byts: + return self._loadCertByts(byts) + + # def loadCertByts(self, byts): + # ''' + # Load a X509 certificate from its PEM encoded bytes. + # + # Args: + # byts (bytes): The PEM encoded bytes of the certificate. + # + # Returns: + # OpenSSL.crypto.X509: The X509 certificate. + # + # Raises: + # BadCertBytes: If the certificate bytes are invalid. + # ''' + # return self._loadCertByts(byts) + # + def _loadCertByts(self, byts: bytes) -> c_x509.Certificate: + try: + return c_x509.load_pem_x509_certificate(byts) + except Exception as e: + logger.exception('UNKNOWN EXCEPTION READING BYTES!') + # FIXME - raise raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') + raise + + # except crypto.Error as e: + # # Unwrap pyopenssl's exception_from_error_queue + # estr = '' + # for argv in e.args: + # if estr: # pragma: no cover + # estr += ', ' + # estr += ' '.join((arg for arg in argv[0] if arg)) + # raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') + + # def _loadCsrPath(self, path): + # byts = self._getPathBytes(path) + # if byts: + # return self._loadCsrByts(byts) + # + # def _loadCsrByts(self, byts): + # return crypto.load_certificate_request(crypto.FILETYPE_PEM, byts) + # + def _loadKeyPath(self, path: AnyStr) -> PkeyOrNoneType: + byts = self._getPathBytes(path) + if byts: + pkey = c_serialization.load_pem_private_key(byts, password=None) + # XXX FIXME Coverage for someone handling in a DER key? + if isinstance(pkey, (c_rsa.RSAPrivateKey, c_dsa.DSAPrivateKey)): + return pkey + # XXX FIXME Coverage for this! + raise s_exc.BadCertBytes(mesg=f'Key at {path} is {type(pkey)}, expected a DSA or RSA key.', + path=path) + # + # def _loadP12Path(self, path): + # byts = self._getPathBytes(path) + # if byts: + # # This API is deprecrated by PyOpenSSL and will need to be rewritten if pyopenssl is + # # updated from v21.x.x. The APIs that use this are not directly exposed via the + # # easycert tool currently, and are only used in unit tests. + # return crypto.load_pkcs12(byts) + # + def _saveCertTo(self, cert: c_x509.Certificate, *paths: AnyStr): + path = self._getPathJoin(*paths) + self._checkDupFile(path) + + with s_common.genfile(path) as fd: + fd.truncate(0) + fd.write(self._certToByts(cert)) + + return path + + def _certToByts(self, cert: c_x509.Certificate): + return cert.public_bytes(encoding=c_serialization.Encoding.PEM) + + def _pkeyToByts(self, pkey: PkeyType) -> ByteString: + return pkey.private_bytes(encoding=c_serialization.Encoding.PEM, + format=c_serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=c_serialization.NoEncryption(), + ) + + def _savePkeyTo(self, pkey: PkeyType, *paths: AnyStr): + path = self._getPathJoin(*paths) + self._checkDupFile(path) + + with s_common.genfile(path) as fd: + fd.truncate(0) + fd.write(self._pkeyToByts(pkey)) + + return path + # + # def _saveP12To(self, cert, *paths): + # path = self._getPathJoin(*paths) + # self._checkDupFile(path) + # + # with s_common.genfile(path) as fd: + # fd.truncate(0) + # fd.write(cert.export()) + # + # return path + +certdirnew = CertDirNew() +def getCertDirnew() -> CertDirNew: + ''' + Get the singleton CertDir instance. + + Returns: + CertDir: A certdir object. + ''' + return certdirnew + +def addCertPathNew(path): + return certdirnew.addCertPath(path) + +def delCertPathNew(path): + return certdirnew.delCertPath(path) + +def getCertDirnnew() -> str: + ''' + Get the expanded default path used by the singleton CertDir instance. + + Returns: + str: The path string. + ''' + return s_common.genpath(defdir) diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 884b0f4ad3..d67e30b214 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -15,8 +15,56 @@ import synapse.lib.crypto.rsa as s_crypto_rsa +import cryptography.x509 as c_x509 +import cryptography.hazmat.primitives.hashes as c_hashes +import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa +import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa +import cryptography.hazmat.primitives.serialization as c_serialization + +class CertDirNewTest(s_t_utils.SynTest): + + @contextmanager + def getCertDir(self): + ''' + Get a test CertDir object. + + Yields: + s_certdir.CertDir: A certdir object based out of a temp directory. + ''' + # create a temp folder and make it a cert dir + with self.getTestDir() as dirname: + yield s_certdir.CertDirNew(path=dirname) + + def test_certdir_cas(self): + + with self.getCertDir() as cdir: # type: s_certdir.CertDiNew + caname = 'syntest' + inter_name = 'testsyn-intermed' + base = cdir._getPathJoin() + + # Test that all the methods for loading the certificates return correct values for non-existant files + self.none(cdir.getCaCert(caname)) + self.none(cdir.getCaKey(caname)) + self.false(cdir.isCaCert(caname)) + self.none(cdir.getCaCertPath(caname)) + self.none(cdir.getCaKeyPath(caname)) + + # Generate a self-signed CA ======================================= + cdir.genCaCert(caname) + + # Test that all the methods for loading the certificates work + self.isinstance(cdir.getCaCert(caname), c_x509.Certificate) + self.isinstance(cdir.getCaKey(caname), c_rsa.RSAPrivateKey) # We do RSA private keys out of the box + self.true(cdir.isCaCert(caname)) + self.eq(cdir.getCaCertPath(caname), base + '/cas/' + caname + '.crt') + self.eq(cdir.getCaKeyPath(caname), base + '/cas/' + caname + '.key') + + class CertDirTest(s_t_utils.SynTest): + def setUp(self) -> None: + self.skip(mesg='Skipping old tests.') + @contextmanager def getCertDir(self): ''' From b6c5344d0e8760489debeec07d56d58f9b7551ef Mon Sep 17 00:00:00 2001 From: epiphyte Date: Fri, 9 Feb 2024 18:19:45 +0000 Subject: [PATCH 03/31] Generally working basicAssumptions :D --- synapse/lib/certdir.py | 58 ++++++++------ synapse/tests/test_lib_certdir.py | 121 +++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 26 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 460882e64d..10689b7c6d 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -2425,31 +2425,39 @@ def isCaCert(self, name: AnyStr) -> bool: # ''' # return self.getUserCertPath(name) is not None # - # def signCertAs(self, cert, signas): - # ''' - # Signs a certificate with a CA keypair. - # - # Args: - # cert (OpenSSL.crypto.X509): The certificate to sign. - # signas (str): The CA keypair name to sign the new keypair with. - # - # Examples: - # Sign a certificate with the CA "myca": - # - # cdir.signCertAs(mycert, 'myca') - # - # Returns: - # None - # ''' - # cakey = self.getCaKey(signas) - # if cakey is None: - # raise s_exc.NoCertKey(mesg=f'Missing .key for {signas}') - # cacert = self.getCaCert(signas) - # if cacert is None: - # raise s_exc.NoCertKey(mesg=f'Missing .crt for {signas}') - # - # cert.set_issuer(cacert.get_subject()) - # cert.sign(cakey, self.signing_digest) + def signCertAs(self, builder: c_x509.CertificateBuilder, signas: AnyStr) -> c_x509.Certificate: + ''' + Signs a certificate with a CA keypair. + + Args: + cert (OpenSSL.crypto.X509): The certificate to sign. + signas (str): The CA keypair name to sign the new keypair with. + + Examples: + Sign a certificate with the CA "myca": + + cdir.signCertAs(mycert, 'myca') + + Returns: + None + ''' + cakey = self.getCaKey(signas) + if cakey is None: + raise s_exc.NoCertKey(mesg=f'Missing .key for {signas}') + cacert = self.getCaCert(signas) + if cacert is None: + raise s_exc.NoCertKey(mesg=f'Missing .crt for {signas}') + + attr = cacert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value + + builder = builder.issuer_name(c_x509.Name([ + c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), + ])) + certificate = builder.sign( + private_key=cakey, algorithm=self.signing_digest(), + ) + return certificate # # def signHostCsr(self, xcsr, signas, outp=None, sans=None, save=True): # ''' diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index d67e30b214..38a0854c4e 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -1,5 +1,6 @@ - import os +import ssl +import datetime from contextlib import contextmanager from OpenSSL import crypto, SSL @@ -16,11 +17,15 @@ import synapse.lib.crypto.rsa as s_crypto_rsa import cryptography.x509 as c_x509 +import cryptography.exceptions as c_exc +import cryptography.x509.verification as c_verification import cryptography.hazmat.primitives.hashes as c_hashes import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa +import cryptography.hazmat.primitives.asymmetric.padding as c_padding import cryptography.hazmat.primitives.serialization as c_serialization + class CertDirNewTest(s_t_utils.SynTest): @contextmanager @@ -35,6 +40,108 @@ def getCertDir(self): with self.getTestDir() as dirname: yield s_certdir.CertDirNew(path=dirname) + def basic_assertions(self, cdir: s_certdir.CertDirNew, + cert: c_x509.Certificate, + key: s_certdir.PkeyType, + cacert: c_x509.Certificate =None): + ''' + test basic certificate assumptions + + Args: + cdir (s_certdir.CertDir): certdir object + cert (crypto.X509): Cert to test + key (crypto.PKey): Key for the certification + cacert (crypto.X509): Corresponding CA cert (optional) + ''' + self.nn(cert) + self.nn(key) + + pubkey = cert.public_key() + + # Make sure the certs were generated with the expected number of bits + self.eq(pubkey.key_size, cdir.crypto_numbits) + self.eq(key.key_size, cdir.crypto_numbits) + + # Make sure the certs were generated with the correct version number + self.eq(cert.version.value, 2) + + # ensure we can sign / verify data with our keypair + buf = b'The quick brown fox jumps over the lazy dog.' + + sig = key.sign(data=buf, + padding=c_padding.PSS(mgf=c_padding.MGF1(c_hashes.SHA256()), + salt_length=c_padding.PSS.MAX_LENGTH), + algorithm=c_hashes.SHA256(), + ) + sig2 = key.sign(data=buf + b'wut', + padding=c_padding.PSS(mgf=c_padding.MGF1(c_hashes.SHA256()), + salt_length=c_padding.PSS.MAX_LENGTH), + algorithm=c_hashes.SHA256(), + ) + + result = pubkey.verify(signature=sig, + data=buf, + padding=c_padding.PSS(mgf=c_padding.MGF1(c_hashes.SHA256()), + salt_length=c_padding.PSS.MAX_LENGTH), + algorithm=c_hashes.SHA256(),) + self.none(result) + + with self.raises(c_exc.InvalidSignature): + pubkey.verify(signature=sig2, + data=buf, + padding=c_padding.PSS(mgf=c_padding.MGF1(c_hashes.SHA256()), + salt_length=c_padding.PSS.MAX_LENGTH), + algorithm=c_hashes.SHA256(), ) + + # XXX FIXME - Figure out a parallel for this in cryptography parlance? + # This is demonstrative of a a high level of control over a SSL Context that + # we don't actually utilize. ??? + # # ensure that a ssl context using both cert/key match + # sslcontext = SSL.Context(SSL.TLSv1_2_METHOD) + # sslcontext.use_certificate(cert) + # sslcontext.use_privatekey(key) + # self.none(sslcontext.check_privatekey()) + + if cacert: + + # Make sure the cert was signed by the CA + cert_issuer = cert.issuer.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + cacert_subj = cacert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + self.eq(cert_issuer, cacert_subj) + + # OpenSSL should NOT be able to verify the certificate if its CA is not loaded + pyopenssl_cert = crypto.X509.from_cryptography(cert) + pyopenssl_cacert = crypto.X509.from_cryptography(cacert) + + store = crypto.X509Store() + ctx = crypto.X509StoreContext(store, pyopenssl_cert) + + # OpenSSL should NOT be able to verify the certificate if its CA is not loaded + store.add_cert(pyopenssl_cert) + + with self.raises(crypto.X509StoreContextError) as cm: + ctx.verify_certificate() + + self.isin('unable to get local issuer certificate', str(cm.exception)) + + # Generate a separate CA that did not sign the certificate + try: + (_, otherca_cert) = cdir.genCaCert('otherca') + except s_exc.DupFileName: + pass + pyopenssl_otherca_cert = crypto.X509.from_cryptography(otherca_cert) + + # OpenSSL should NOT be able to verify the certificate if its CA is not loaded + store.add_cert(pyopenssl_otherca_cert) + with self.raises(crypto.X509StoreContextError) as cm: + # unable to get local issuer certificate + ctx.verify_certificate() + self.isin('unable to get local issuer certificate', str(cm.exception)) + + # OpenSSL should be able to verify the certificate, once its CA is loaded + store.add_cert(pyopenssl_cacert) + self.none(ctx.verify_certificate()) # valid + def test_certdir_cas(self): with self.getCertDir() as cdir: # type: s_certdir.CertDiNew @@ -59,6 +166,18 @@ def test_certdir_cas(self): self.eq(cdir.getCaCertPath(caname), base + '/cas/' + caname + '.crt') self.eq(cdir.getCaKeyPath(caname), base + '/cas/' + caname + '.key') + # Run basic assertions on the CA keypair + cacert = cdir.getCaCert(caname) + cakey = cdir.getCaKey(caname) + self.basic_assertions(cdir, cacert, cakey) + + # Generate intermediate CA ======================================== + cdir.genCaCert(inter_name, signas=caname) + + # Run basic assertions, make sure that it was signed by the root CA + inter_cacert = cdir.getCaCert(inter_name) + inter_cakey = cdir.getCaKey(inter_name) + self.basic_assertions(cdir, inter_cacert, inter_cakey, cacert=cacert) class CertDirTest(s_t_utils.SynTest): From e6e848a22e9674278efe0e9d7cb4f67cb15ad0d0 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Fri, 9 Feb 2024 23:17:33 +0000 Subject: [PATCH 04/31] More progress --- synapse/lib/certdir.py | 353 ++++++++++++++++-------------- synapse/tests/test_lib_certdir.py | 96 +++++++- 2 files changed, 281 insertions(+), 168 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 10689b7c6d..8bee6f2ab1 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -8,7 +8,7 @@ import datetime import collections -from typing import AnyStr, ByteString, Optional, Tuple, Union +from typing import AnyStr, ByteString, List, Optional, Tuple, Union from OpenSSL import crypto # type: ignore @@ -1604,58 +1604,77 @@ def genCaCert(self, name: AnyStr, outp.printf('cert saved: %s' % (crtpath,)) return pkey, cert - # - # def genHostCert(self, name, signas=None, outp=None, csr=None, sans=None, save=True): - # ''' - # Generates a host keypair. - # - # Args: - # name (str): The name of the host keypair. - # signas (str): The CA keypair to sign the new host keypair with. - # outp (synapse.lib.output.Output): The output buffer. - # csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. - # sans (list): List of subject alternative names. - # - # Examples: - # Make a host keypair named "myhost": - # - # myhostkey, myhostcert = cdir.genHostCert('myhost') - # - # Returns: - # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the private key and certificate objects. - # ''' - # pkey, cert = self._genBasePkeyCert(name, pkey=csr) - # - # ext_sans = {'DNS:' + name} - # if isinstance(sans, str): - # ext_sans = ext_sans.union(sans.split(',')) - # ext_sans = ','.join(sorted(ext_sans)) - # - # cert.add_extensions([ - # crypto.X509Extension(b'nsCertType', False, b'server'), - # crypto.X509Extension(b'keyUsage', False, b'digitalSignature,keyEncipherment'), - # crypto.X509Extension(b'extendedKeyUsage', False, b'serverAuth'), - # crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE'), - # crypto.X509Extension(b'subjectAltName', False, ext_sans.encode('utf-8')), - # ]) - # - # if signas is not None: - # self.signCertAs(cert, signas) - # else: - # self.selfSignCert(cert, pkey) - # - # if save: - # if not pkey._only_public: - # keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) - # if outp is not None: - # outp.printf('key saved: %s' % (keypath,)) - # - # crtpath = self._saveCertTo(cert, 'hosts', '%s.crt' % name) - # if outp is not None: - # outp.printf('cert saved: %s' % (crtpath,)) - # - # return pkey, cert - # + + def genHostCert(self, name: AnyStr, + signas: Optional[AnyStr | None] = None, + outp: s_output.OutPut = None, + csr: PkeyOrNoneType =None, + sans: List[AnyStr] =None, + save: bool = True) -> PkeyAndCertType: + ''' + Generates a host keypair. + + Args: + name (str): The name of the host keypair. + signas (str): The CA keypair to sign the new host keypair with. + outp (synapse.lib.output.Output): The output buffer. + csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. + sans (list): List of subject alternative names. + + Examples: + Make a host keypair named "myhost": + + myhostkey, myhostcert = cdir.genHostCert('myhost') + + Returns: + ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the private key and certificate objects. + ''' + pkey, builder = self._genBasePkeyCert(name, pkey=csr) + + # XXX FIXME - Sort out some generic SANS support from pyopenssl loose apis + # ext_sans = {'DNS:' + name} + # if isinstance(sans, str): + # ext_sans = ext_sans.union(sans.split(',')) + # ext_sans = [c_x509.GeneralName(valu) for valu in sorted(ext_sans)] + + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) + builder = builder.add_extension( + c_x509.KeyUsage(digital_signature=True, key_encipherment=True, data_encipherment=False, key_agreement=False, + key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, + content_commitment=False), + critical=False, + ) + builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), critical=False) + builder = builder.add_extension(c_x509.UnrecognizedExtension( + oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@'), + critical=False, + ) + + # XXX FIXME - Sort out some generic SANS support from pyopenssl loose apis + # builder = builder.add_extension(c_x509.SubjectAlternativeName(ext_sans), critical=False) + + if signas is not None: + cert = self.signCertAs(builder, signas) + else: + cert = self.selfSignCert(builder, pkey) + + if save: + # XXX FIXME - This code refers to a private member of the old openssl Pkey class :( + # Lets let this fly until its a problem. + # if not pkey._only_public: + # keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) + # if outp is not None: + # outp.printf('key saved: %s' % (keypath,)) + keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) + if outp is not None: + outp.printf('key saved: %s' % (keypath,)) + + crtpath = self._saveCertTo(cert, 'hosts', '%s.crt' % name) + if outp is not None: + outp.printf('cert saved: %s' % (crtpath,)) + + return pkey, cert + # def genHostCsr(self, name, outp=None): # ''' # Generates a host certificate signing request. @@ -2087,43 +2106,43 @@ def getCaKeyPath(self, name: AnyStr) -> StrOrNoneType: # if os.path.isfile(path): # return path # - # def getHostCaPath(self, name): - # ''' - # Gets the path to the CA certificate that issued a given host keypair. - # - # Args: - # name (str): The name of the host keypair. - # - # Examples: - # Get the path to the CA cert which issue the cert for "myhost": - # - # mypath = cdir.getHostCaPath('myhost') - # - # Returns: - # str: The path if exists. - # ''' - # cert = self.getHostCert(name) - # if cert is None: - # return None - # - # return self._getCaPath(cert) - # - # def getHostCert(self, name): - # ''' - # Loads the X509 object for a given host keypair. - # - # Args: - # name (str): The name of the host keypair. - # - # Examples: - # Get the certificate object for the host "myhost": - # - # myhostcert = cdir.getHostCert('myhost') - # - # Returns: - # OpenSSL.crypto.X509: The certificate, if exists. - # ''' - # return self._loadCertPath(self.getHostCertPath(name)) + def getHostCaPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to the CA certificate that issued a given host keypair. + + Args: + name (str): The name of the host keypair. + + Examples: + Get the path to the CA cert which issue the cert for "myhost": + + mypath = cdir.getHostCaPath('myhost') + + Returns: + str: The path if exists. + ''' + cert = self.getHostCert(name) + if cert is None: + return None + + return self._getCaPath(cert) + + def getHostCert(self, name: AnyStr) -> CertOrNoneType: + ''' + Loads the X509 object for a given host keypair. + + Args: + name (str): The name of the host keypair. + + Examples: + Get the certificate object for the host "myhost": + + myhostcert = cdir.getHostCert('myhost') + + Returns: + OpenSSL.crypto.X509: The certificate, if exists. + ''' + return self._loadCertPath(self.getHostCertPath(name)) # # def getHostCertHash(self, name): # cert = self.getHostCert(name) @@ -2131,63 +2150,63 @@ def getCaKeyPath(self, name: AnyStr) -> StrOrNoneType: # return None # return cert.digest('SHA256').decode().lower().replace(':', '') # - # def getHostCertPath(self, name): - # ''' - # Gets the path to a host certificate. - # - # Args: - # name (str): The name of the host keypair. - # - # Examples: - # Get the path to the host certificate for the host "myhost": - # - # mypath = cdir.getHostCertPath('myhost') - # - # Returns: - # str: The path if exists. - # ''' - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'hosts', '%s.crt' % name) - # if os.path.isfile(path): - # return path - # - # def getHostKey(self, name): - # ''' - # Loads the PKey object for a given host keypair. - # - # Args: - # name (str): The name of the host keypair. - # - # Examples: - # Get the private key object for the host "myhost": - # - # myhostkey = cdir.getHostKey('myhost') - # - # Returns: - # OpenSSL.crypto.PKey: The private key, if exists. - # ''' - # return self._loadKeyPath(self.getHostKeyPath(name)) - # - # def getHostKeyPath(self, name): - # ''' - # Gets the path to a host key. - # - # Args: - # name (str): The name of the host keypair. - # - # Examples: - # Get the path to the host key for the host "myhost": - # - # mypath = cdir.getHostKeyPath('myhost') - # - # Returns: - # str: The path if exists. - # ''' - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'hosts', '%s.key' % name) - # if os.path.isfile(path): - # return path - # + def getHostCertPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to a host certificate. + + Args: + name (str): The name of the host keypair. + + Examples: + Get the path to the host certificate for the host "myhost": + + mypath = cdir.getHostCertPath('myhost') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'hosts', '%s.crt' % name) + if os.path.isfile(path): + return path + + def getHostKey(self, name: AnyStr) -> PkeyOrNoneType: + ''' + Loads the PKey object for a given host keypair. + + Args: + name (str): The name of the host keypair. + + Examples: + Get the private key object for the host "myhost": + + myhostkey = cdir.getHostKey('myhost') + + Returns: + OpenSSL.crypto.PKey: The private key, if exists. + ''' + return self._loadKeyPath(self.getHostKeyPath(name)) + + def getHostKeyPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to a host key. + + Args: + name (str): The name of the host keypair. + + Examples: + Get the path to the host key for the host "myhost": + + mypath = cdir.getHostKeyPath('myhost') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'hosts', '%s.key' % name) + if os.path.isfile(path): + return path + # def getUserCaPath(self, name): # ''' # Gets the path to the CA certificate that issued a given user keypair. @@ -2391,22 +2410,22 @@ def isCaCert(self, name: AnyStr) -> bool: # crtpath = self._getPathJoin('users', '%s.p12' % name) # return os.path.isfile(crtpath) # - # def isHostCert(self, name): - # ''' - # Checks if a host certificate exists. - # - # Args: - # name (str): The name of the host keypair. - # - # Examples: - # Check if the host cert "myhost" exists: - # - # exists = cdir.isUserCert('myhost') - # - # Returns: - # bool: True if the certificate is present, False otherwise. - # ''' - # return self.getHostCertPath(name) is not None + def isHostCert(self, name: AnyStr) -> bool: + ''' + Checks if a host certificate exists. + + Args: + name (str): The name of the host keypair. + + Examples: + Check if the host cert "myhost" exists: + + exists = cdir.isUserCert('myhost') + + Returns: + bool: True if the certificate is present, False otherwise. + ''' + return self.getHostCertPath(name) is not None # # def isUserCert(self, name): # ''' @@ -2741,10 +2760,10 @@ def _genBasePkeyCert(self, name: AnyStr, pkey: PkeyOrNoneType =None) -> PkeyAndB # # return byts # - # def _getCaPath(self, cert): - # subj = cert.get_issuer() - # return self.getCaCertPath(subj.CN) - # + def _getCaPath(self, cert: c_x509.Certificate) -> StrOrNoneType: + issuer = cert.issuer.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + return self.getCaCertPath(issuer.value) + def _getPathBytes(self, path: AnyStr) -> BytesOrNoneType: if path is None: return None diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 38a0854c4e..15e0cea192 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -40,7 +40,8 @@ def getCertDir(self): with self.getTestDir() as dirname: yield s_certdir.CertDirNew(path=dirname) - def basic_assertions(self, cdir: s_certdir.CertDirNew, + def basic_assertions(self, + cdir: s_certdir.CertDirNew, cert: c_x509.Certificate, key: s_certdir.PkeyType, cacert: c_x509.Certificate =None): @@ -142,6 +143,45 @@ def basic_assertions(self, cdir: s_certdir.CertDirNew, store.add_cert(pyopenssl_cacert) self.none(ctx.verify_certificate()) # valid + def host_assertions(self, + cdir: s_certdir.CertDirNew, + cert: c_x509.Certificate, + key: s_certdir.PkeyType, + cacert: c_x509.Certificate = None): + ''' + test basic certificate assumptions for a host certificate + + Args: + cdir (s_certdir.CertDir): certdir object + cert (crypto.X509): Cert to test + key (crypto.PKey): Key for the certification + cacert (crypto.X509): Corresponding CA cert (optional) + ''' + # XXX FIXME There is a schism between teh items build for use with the builder + # interface nd the items parsed from a certificate on disk :\ + # exts = {} + # for ext in cert.extensions: # type: c_x509.Extension + # self.false(ext.critical) + # short_name = ext.oid._name + # if short_name == 'Unknown OID': + # short_name = ext.oid.dotted_string + # exts[short_name] = ext + # + # nscertext = c_x509.UnrecognizedExtension( + # oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@') + # keyuseext = c_x509.KeyUsage(digital_signature=True, key_encipherment=True, data_encipherment=False, key_agreement=False, + # key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, + # content_commitment=False) + # print(keyuseext.public_bytes()) + # extkeyuseext = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]) + # basicconext = c_x509.BasicConstraints(ca=False, path_length=None) + # + # # self.eq(exts['2.16.840.1.113730.1.1'], nscertext) + # self.eq(exts['keyUsage'], keyuseext) + # # self.eq(exts[b'extendedKeyUsage'], extkeyuseext.get_data()) + # # self.eq(exts[b'basicConstraints'], basicconext.get_data()) + # # self.isin(b'subjectAltName', exts) + def test_certdir_cas(self): with self.getCertDir() as cdir: # type: s_certdir.CertDiNew @@ -179,6 +219,60 @@ def test_certdir_cas(self): inter_cakey = cdir.getCaKey(inter_name) self.basic_assertions(cdir, inter_cacert, inter_cakey, cacert=cacert) + def test_certdir_hosts(self): + with self.getCertDir() as cdir: # type: s_certdir.CertDir + caname = 'syntest' + hostname = 'visi.vertex.link' + hostname_unsigned = 'unsigned.vertex.link' + base = cdir._getPathJoin() + + cdir.genCaCert(caname) + + cacert = cdir.getCaCert(caname) + + # Test that all the methods for loading the certificates return correct values for non-existant files + self.none(cdir.getHostCert(hostname_unsigned)) + self.none(cdir.getHostKey(hostname_unsigned)) + self.false(cdir.isHostCert(hostname_unsigned)) + self.none(cdir.getHostCertPath(hostname_unsigned)) + self.none(cdir.getHostKeyPath(hostname_unsigned)) + self.none(cdir.getHostCaPath(hostname_unsigned)) + + # Generate a self-signed host keypair ============================= + cdir.genHostCert(hostname_unsigned) + + # Test that all the methods for loading the certificates work + self.isinstance(cdir.getHostCert(hostname_unsigned), c_x509.Certificate) + self.isinstance(cdir.getHostKey(hostname_unsigned), c_rsa.RSAPrivateKey) + self.true(cdir.isHostCert(hostname_unsigned)) + self.eq(cdir.getHostCertPath(hostname_unsigned), base + '/hosts/' + hostname_unsigned + '.crt') + self.eq(cdir.getHostKeyPath(hostname_unsigned), base + '/hosts/' + hostname_unsigned + '.key') + self.none(cdir.getHostCaPath(hostname_unsigned)) # the cert is self-signed, so there is no ca cert + + # Run basic assertions on the host keypair + cert = cdir.getHostCert(hostname_unsigned) + key = cdir.getHostKey(hostname_unsigned) + self.basic_assertions(cdir, cert, key) + self.host_assertions(cdir, cert, key) + + # Generate a signed host keypair ================================== + cdir.genHostCert(hostname, signas=caname) + + # Test that all the methods for loading the certificates work + self.isinstance(cdir.getHostCert(hostname), c_x509.Certificate) + self.isinstance(cdir.getHostKey(hostname), c_rsa.RSAPrivateKey) + self.true(cdir.isHostCert(hostname)) + self.eq(cdir.getHostCertPath(hostname), base + '/hosts/' + hostname + '.crt') + self.eq(cdir.getHostKeyPath(hostname), base + '/hosts/' + hostname + '.key') + self.eq(cdir.getHostCaPath(hostname), base + '/cas/' + caname + '.crt') # the cert is signed, so there is a ca cert + + # Run basic assertions on the host keypair + cert = cdir.getHostCert(hostname) + key = cdir.getHostKey(hostname) + self.basic_assertions(cdir, cert, key, cacert=cacert) + self.host_assertions(cdir, cert, key, cacert=cacert) + + class CertDirTest(s_t_utils.SynTest): def setUp(self) -> None: From b7ba9beea16eb1b7869167d202fca2136fb65b43 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Mon, 12 Feb 2024 02:20:48 +0000 Subject: [PATCH 05/31] Fix up User test implementation --- synapse/lib/certdir.py | 607 +++++++++++++++--------------- synapse/tests/test_lib_certdir.py | 153 +++++++- 2 files changed, 464 insertions(+), 296 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 8bee6f2ab1..9f46a1f3dd 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -22,6 +22,7 @@ import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa import cryptography.hazmat.primitives.serialization as c_serialization +import cryptography.hazmat.primitives.serialization.pkcs12 as c_pkcs12 defdir_default = '~/.syn/certs' defdir = os.getenv('SYN_CERT_DIR') @@ -40,6 +41,7 @@ PkeyAndCertType = Tuple[c_rsa.RSAPrivateKey, c_x509.Certificate] PkeyAndBuilderType = Tuple[c_rsa.RSAPrivateKey, c_x509.CertificateBuilder] PkeyType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey] +Pkcs12OrNoneType = Union[c_pkcs12.PKCS12KeyAndCertificates | None] def iterFqdnUp(fqdn): levs = fqdn.split('.') @@ -1693,49 +1695,69 @@ def genHostCert(self, name: AnyStr, # ''' # return self._genPkeyCsr(name, 'hosts', outp=outp) # - # def genUserCert(self, name, signas=None, outp=None, csr=None, save=True): - # ''' - # Generates a user keypair. - # - # Args: - # name (str): The name of the user keypair. - # signas (str): The CA keypair to sign the new user keypair with. - # outp (synapse.lib.output.Output): The output buffer. - # csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. - # - # Examples: - # Generate a user cert for the user "myuser": - # - # myuserkey, myusercert = cdir.genUserCert('myuser') - # - # Returns: - # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. - # ''' - # pkey, cert = self._genBasePkeyCert(name, pkey=csr) - # - # cert.add_extensions([ - # crypto.X509Extension(b'nsCertType', False, b'client'), - # crypto.X509Extension(b'keyUsage', False, b'digitalSignature'), - # crypto.X509Extension(b'extendedKeyUsage', False, b'clientAuth'), - # crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE'), - # ]) - # - # if signas is not None: - # self.signCertAs(cert, signas) - # else: - # self.selfSignCert(cert, pkey) - # - # if save: - # crtpath = self._saveCertTo(cert, 'users', '%s.crt' % name) - # if outp is not None: - # outp.printf('cert saved: %s' % (crtpath,)) - # - # if not pkey._only_public: - # keypath = self._savePkeyTo(pkey, 'users', '%s.key' % name) - # if outp is not None: - # outp.printf('key saved: %s' % (keypath,)) - # - # return pkey, cert + def genUserCert(self, + name: AnyStr, + signas: Optional[AnyStr | None] = None, + outp: s_output.OutPut = None, + csr: PkeyOrNoneType =None, + save: bool = True) -> PkeyAndCertType: + ''' + Generates a user keypair. + + Args: + name (str): The name of the user keypair. + signas (str): The CA keypair to sign the new user keypair with. + outp (synapse.lib.output.Output): The output buffer. + csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. + + Examples: + Generate a user cert for the user "myuser": + + myuserkey, myusercert = cdir.genUserCert('myuser') + + Returns: + ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. + ''' + pkey, builder = self._genBasePkeyCert(name, pkey=csr) + + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) + builder = builder.add_extension( + c_x509.KeyUsage(digital_signature=True, key_encipherment=False, data_encipherment=False, key_agreement=False, + key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, + content_commitment=False), + critical=False, + ) + builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]), + critical=False) + # XXX FIXME Client encoding value for + # crypto.X509Extension(b'nsCertType', False, b'client'), + # builder = builder.add_extension(c_x509.UnrecognizedExtension( + # oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@'), + # critical=False, + # ) + + if signas is not None: + cert = self.signCertAs(builder, signas) + else: + cert = self.selfSignCert(builder, pkey) + + if save: + # XXX FIXME - This code refers to a private member of the old openssl Pkey class :( + # Lets let this fly until its a problem. + # if not pkey._only_public: + # keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) + # if outp is not None: + # outp.printf('key saved: %s' % (keypath,)) + keypath = self._savePkeyTo(pkey, 'users', '%s.key' % name) + if outp is not None: + outp.printf('key saved: %s' % (keypath,)) + + crtpath = self._saveCertTo(cert, 'users', '%s.crt' % name) + if outp is not None: + outp.printf('cert saved: %s' % (crtpath,)) + + return pkey, cert + # # def genCodeCert(self, name, signas=None, outp=None, save=True): # ''' @@ -1872,45 +1894,44 @@ def genHostCert(self, name: AnyStr, # # return crls # - # def genClientCert(self, name, outp=None): - # ''' - # Generates a user PKCS #12 archive. - # Please note that the resulting file will contain private key material. - # - # Args: - # name (str): The name of the user keypair. - # outp (synapse.lib.output.Output): The output buffer. - # - # Examples: - # Make the PKC12 object for user "myuser": - # - # myuserpkcs12 = cdir.genClientCert('myuser') - # - # Returns: - # OpenSSL.crypto.PKCS12: The PKCS #12 archive. - # ''' - # ucert = self.getUserCert(name) - # if not ucert: - # raise s_exc.NoSuchFile(mesg='missing User cert', name=name) - # - # capath = self._getCaPath(ucert) - # cacert = self._loadCertPath(capath) - # if not cacert: - # raise s_exc.NoSuchFile(mesg='missing CA cert', path=capath) - # - # ukey = self.getUserKey(name) - # if not ukey: - # raise s_exc.NoSuchFile(mesg='missing User private key', name=name) - # - # ccert = crypto.PKCS12() - # ccert.set_friendlyname(name.encode('utf-8')) - # ccert.set_ca_certificates([cacert]) - # ccert.set_certificate(ucert) - # ccert.set_privatekey(ukey) - # - # crtpath = self._saveP12To(ccert, 'users', '%s.p12' % name) - # if outp is not None: - # outp.printf('client cert saved: %s' % (crtpath,)) + def genClientCert(self, name: AnyStr, outp: s_output.OutPut =None) -> None: + ''' + Generates a user PKCS #12 archive. + Please note that the resulting file will contain private key material. + + Args: + name (str): The name of the user keypair. + outp (synapse.lib.output.Output): The output buffer. + + Examples: + Make the PKC12 object for user "myuser": + + myuserpkcs12 = cdir.genClientCert('myuser') + + Returns: + None + ''' + ucert = self.getUserCert(name) + if not ucert: + raise s_exc.NoSuchFile(mesg='missing User cert', name=name) + + capath = self._getCaPath(ucert) + cacert = self._loadCertPath(capath) + if not cacert: + raise s_exc.NoSuchFile(mesg='missing CA cert', path=capath) + + ukey = self.getUserKey(name) + if not ukey: + raise s_exc.NoSuchFile(mesg='missing User private key', name=name) + + byts = c_pkcs12.serialize_key_and_certificates(name=name.encode('utf-8'), + key=ukey, + cert=ucert, + cas=[cacert], + encryption_algorithm=c_serialization.NoEncryption()) + crtpath = self._saveP12To(byts, 'users', '%s.p12' % name) + if outp is not None: + outp.printf('client cert saved: %s' % (crtpath,)) # # def valUserCert(self, byts, cacerts=None): # ''' @@ -2066,46 +2087,46 @@ def getCaKeyPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - # def getClientCert(self, name): - # ''' - # Loads the PKCS12 archive object for a given user keypair. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Get the PKCS12 object for the user "myuser": - # - # mypkcs12 = cdir.getClientCert('myuser') - # - # Notes: - # The PKCS12 archive will contain private key material if it was created with CertDir or the easycert tool - # - # Returns: - # OpenSSL.crypto.PKCS12: The PKCS12 archive, if exists. - # ''' - # return self._loadP12Path(self.getClientCertPath(name)) - # - # def getClientCertPath(self, name): - # ''' - # Gets the path to a client certificate. - # - # Args: - # name (str): The name of the client keypair. - # - # Examples: - # Get the path to the client certificate for "myuser": - # - # mypath = cdir.getClientCertPath('myuser') - # - # Returns: - # str: The path if exists. - # ''' - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'users', '%s.p12' % name) - # if os.path.isfile(path): - # return path - # + def getClientCert(self, name: AnyStr) -> Pkcs12OrNoneType: + ''' + Loads the PKCS12 archive object for a given user keypair. + + Args: + name (str): The name of the user keypair. + + Examples: + Get the PKCS12 object for the user "myuser": + + mypkcs12 = cdir.getClientCert('myuser') + + Notes: + The PKCS12 archive will contain private key material if it was created with CertDir or the easycert tool + + Returns: + OpenSSL.crypto.PKCS12: The PKCS12 archive, if exists. + ''' + return self._loadP12Path(self.getClientCertPath(name)) + + def getClientCertPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to a client certificate. + + Args: + name (str): The name of the client keypair. + + Examples: + Get the path to the client certificate for "myuser": + + mypath = cdir.getClientCertPath('myuser') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'users', '%s.p12' % name) + if os.path.isfile(path): + return path + def getHostCaPath(self, name: AnyStr) -> StrOrNoneType: ''' Gets the path to the CA certificate that issued a given host keypair. @@ -2207,122 +2228,122 @@ def getHostKeyPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - # def getUserCaPath(self, name): - # ''' - # Gets the path to the CA certificate that issued a given user keypair. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Get the path to the CA cert which issue the cert for "myuser": - # - # mypath = cdir.getUserCaPath('myuser') - # - # Returns: - # str: The path if exists. - # ''' - # cert = self.getUserCert(name) - # if cert is None: - # return None - # - # return self._getCaPath(cert) - # - # def getUserCert(self, name): - # ''' - # Loads the X509 object for a given user keypair. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Get the certificate object for the user "myuser": - # - # myusercert = cdir.getUserCert('myuser') - # - # Returns: - # OpenSSL.crypto.X509: The certificate, if exists. - # ''' - # return self._loadCertPath(self.getUserCertPath(name)) - # - # def getUserCertPath(self, name): - # ''' - # Gets the path to a user certificate. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Get the path for the user cert for "myuser": - # - # mypath = cdir.getUserCertPath('myuser') - # - # Returns: - # str: The path if exists. - # ''' - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'users', '%s.crt' % name) - # if os.path.isfile(path): - # return path - # - # def getUserForHost(self, user, host): - # ''' - # Gets the name of the first existing user cert for a given user and host. - # - # Args: - # user (str): The name of the user. - # host (str): The name of the host. - # - # Examples: - # Get the name for the "myuser" user cert at "cool.vertex.link": - # - # usercertname = cdir.getUserForHost('myuser', 'cool.vertex.link') - # - # Returns: - # str: The cert name, if exists. - # ''' - # for name in iterFqdnUp(host): - # usercert = '%s@%s' % (user, name) - # if self.isUserCert(usercert): - # return usercert - # - # def getUserKey(self, name): - # ''' - # Loads the PKey object for a given user keypair. - # - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Get the key object for the user key for "myuser": - # - # myuserkey = cdir.getUserKey('myuser') - # - # Returns: - # OpenSSL.crypto.PKey: The private key, if exists. - # ''' - # return self._loadKeyPath(self.getUserKeyPath(name)) - # - # def getUserKeyPath(self, name): - # ''' - # Gets the path to a user key. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Get the path to the user key for "myuser": - # - # mypath = cdir.getUserKeyPath('myuser') - # - # Returns: - # str: The path if exists. - # ''' - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'users', '%s.key' % name) - # if os.path.isfile(path): - # return path + def getUserCaPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to the CA certificate that issued a given user keypair. + + Args: + name (str): The name of the user keypair. + + Examples: + Get the path to the CA cert which issue the cert for "myuser": + + mypath = cdir.getUserCaPath('myuser') + + Returns: + str: The path if exists. + ''' + cert = self.getUserCert(name) + if cert is None: + return None + + return self._getCaPath(cert) + + def getUserCert(self, name: AnyStr) -> CertOrNoneType: + ''' + Loads the X509 object for a given user keypair. + + Args: + name (str): The name of the user keypair. + + Examples: + Get the certificate object for the user "myuser": + + myusercert = cdir.getUserCert('myuser') + + Returns: + OpenSSL.crypto.X509: The certificate, if exists. + ''' + return self._loadCertPath(self.getUserCertPath(name)) + + def getUserCertPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to a user certificate. + + Args: + name (str): The name of the user keypair. + + Examples: + Get the path for the user cert for "myuser": + + mypath = cdir.getUserCertPath('myuser') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'users', '%s.crt' % name) + if os.path.isfile(path): + return path + + def getUserForHost(self, user: AnyStr, host: AnyStr) -> StrOrNoneType: + ''' + Gets the name of the first existing user cert for a given user and host. + + Args: + user (str): The name of the user. + host (str): The name of the host. + + Examples: + Get the name for the "myuser" user cert at "cool.vertex.link": + + usercertname = cdir.getUserForHost('myuser', 'cool.vertex.link') + + Returns: + str: The cert name, if exists. + ''' + for name in iterFqdnUp(host): + usercert = '%s@%s' % (user, name) + if self.isUserCert(usercert): + return usercert + + def getUserKey(self, name: AnyStr) -> CertOrNoneType: + ''' + Loads the PKey object for a given user keypair. + + + Args: + name (str): The name of the user keypair. + + Examples: + Get the key object for the user key for "myuser": + + myuserkey = cdir.getUserKey('myuser') + + Returns: + OpenSSL.crypto.PKey: The private key, if exists. + ''' + return self._loadKeyPath(self.getUserKeyPath(name)) + + def getUserKeyPath(self, name: AnyStr) -> StrOrNoneType: + ''' + Gets the path to a user key. + + Args: + name (str): The name of the user keypair. + + Examples: + Get the path to the user key for "myuser": + + mypath = cdir.getUserKeyPath('myuser') + + Returns: + str: The path if exists. + ''' + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'users', '%s.key' % name) + if os.path.isfile(path): + return path # # def getUserCsrPath(self, name): # for cdir in self.certdirs: @@ -2391,25 +2412,25 @@ def isCaCert(self, name: AnyStr) -> bool: bool: True if the certificate is present, False otherwise. ''' return self.getCaCertPath(name) is not None - # - # def isClientCert(self, name): - # ''' - # Checks if a user client certificate (PKCS12) exists. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Check if the client certificate "myuser" exists: - # - # exists = cdir.isClientCert('myuser') - # - # Returns: - # bool: True if the certificate is present, False otherwise. - # ''' - # crtpath = self._getPathJoin('users', '%s.p12' % name) - # return os.path.isfile(crtpath) - # + + def isClientCert(self, name: AnyStr) -> bool: + ''' + Checks if a user client certificate (PKCS12) exists. + + Args: + name (str): The name of the user keypair. + + Examples: + Check if the client certificate "myuser" exists: + + exists = cdir.isClientCert('myuser') + + Returns: + bool: True if the certificate is present, False otherwise. + ''' + crtpath = self._getPathJoin('users', '%s.p12' % name) + return os.path.isfile(crtpath) + def isHostCert(self, name: AnyStr) -> bool: ''' Checks if a host certificate exists. @@ -2426,24 +2447,24 @@ def isHostCert(self, name: AnyStr) -> bool: bool: True if the certificate is present, False otherwise. ''' return self.getHostCertPath(name) is not None - # - # def isUserCert(self, name): - # ''' - # Checks if a user certificate exists. - # - # Args: - # name (str): The name of the user keypair. - # - # Examples: - # Check if the user cert "myuser" exists: - # - # exists = cdir.isUserCert('myuser') - # - # Returns: - # bool: True if the certificate is present, False otherwise. - # ''' - # return self.getUserCertPath(name) is not None - # + + def isUserCert(self, name: AnyStr) -> bool: + ''' + Checks if a user certificate exists. + + Args: + name (str): The name of the user keypair. + + Examples: + Check if the user cert "myuser" exists: + + exists = cdir.isUserCert('myuser') + + Returns: + bool: True if the certificate is present, False otherwise. + ''' + return self.getUserCertPath(name) is not None + def signCertAs(self, builder: c_x509.CertificateBuilder, signas: AnyStr) -> c_x509.Certificate: ''' Signs a certificate with a CA keypair. @@ -2828,15 +2849,13 @@ def _loadKeyPath(self, path: AnyStr) -> PkeyOrNoneType: # XXX FIXME Coverage for this! raise s_exc.BadCertBytes(mesg=f'Key at {path} is {type(pkey)}, expected a DSA or RSA key.', path=path) - # - # def _loadP12Path(self, path): - # byts = self._getPathBytes(path) - # if byts: - # # This API is deprecrated by PyOpenSSL and will need to be rewritten if pyopenssl is - # # updated from v21.x.x. The APIs that use this are not directly exposed via the - # # easycert tool currently, and are only used in unit tests. - # return crypto.load_pkcs12(byts) - # + + def _loadP12Path(self, path: AnyStr) -> Pkcs12OrNoneType: + byts = self._getPathBytes(path) + if byts: + p12 = c_pkcs12.load_pkcs12(byts, password=None) + return p12 + def _saveCertTo(self, cert: c_x509.Certificate, *paths: AnyStr): path = self._getPathJoin(*paths) self._checkDupFile(path) @@ -2865,16 +2884,16 @@ def _savePkeyTo(self, pkey: PkeyType, *paths: AnyStr): fd.write(self._pkeyToByts(pkey)) return path - # - # def _saveP12To(self, cert, *paths): - # path = self._getPathJoin(*paths) - # self._checkDupFile(path) - # - # with s_common.genfile(path) as fd: - # fd.truncate(0) - # fd.write(cert.export()) - # - # return path + + def _saveP12To(self, byts, *paths): + path = self._getPathJoin(*paths) + self._checkDupFile(path) + + with s_common.genfile(path) as fd: + fd.truncate(0) + fd.write(byts) + + return path certdirnew = CertDirNew() def getCertDirnew() -> CertDirNew: diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 15e0cea192..74cb099c27 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -24,6 +24,7 @@ import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa import cryptography.hazmat.primitives.asymmetric.padding as c_padding import cryptography.hazmat.primitives.serialization as c_serialization +import cryptography.hazmat.primitives.serialization.pkcs12 as c_pkcs12 class CertDirNewTest(s_t_utils.SynTest): @@ -128,8 +129,8 @@ def basic_assertions(self, # Generate a separate CA that did not sign the certificate try: (_, otherca_cert) = cdir.genCaCert('otherca') - except s_exc.DupFileName: - pass + except s_exc.DupFileName as e: + otherca_cert = cdir.getCaCert('otherca') pyopenssl_otherca_cert = crypto.X509.from_cryptography(otherca_cert) # OpenSSL should NOT be able to verify the certificate if its CA is not loaded @@ -182,6 +183,81 @@ def host_assertions(self, # # self.eq(exts[b'basicConstraints'], basicconext.get_data()) # # self.isin(b'subjectAltName', exts) + def user_assertions(self, + cdir: s_certdir.CertDirNew, + cert: c_x509.Certificate, + key: s_certdir.PkeyType, + cacert: c_x509.Certificate = None): + ''' + test basic certificate assumptions for a user certificate + + Args: + cdir (s_certdir.CertDir): certdir object + cert (crypto.X509): Cert to test + key (crypto.PKey): Key for the certification + cacert (crypto.X509): Corresponding CA cert (optional) + ''' + # XXX FIXME There is a schism between teh items build for use with the builder + # interface nd the items parsed from a certificate on disk :\ + # nextensions = cert.get_extension_count() + # exts = {ext.get_short_name(): ext.get_data() for ext in [cert.get_extension(i) for i in range(nextensions)]} + # + # nscertext = crypto.X509Extension(b'nsCertType', False, b'client') + # keyuseext = crypto.X509Extension(b'keyUsage', False, b'digitalSignature') + # extkeyuseext = crypto.X509Extension(b'extendedKeyUsage', False, b'clientAuth') + # basicconext = crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE') + # self.eq(exts[b'nsCertType'], nscertext.get_data()) + # self.eq(exts[b'keyUsage'], keyuseext.get_data()) + # self.eq(exts[b'extendedKeyUsage'], extkeyuseext.get_data()) + # self.eq(exts[b'basicConstraints'], basicconext.get_data()) + # self.notin(b'subjectAltName', exts) + + def p12_assertions(self, + cdir: s_certdir.CertDirNew, + cert: c_x509.Certificate, + key: s_certdir.PkeyType, + p12: c_pkcs12.PKCS12KeyAndCertificates, + cacert: c_x509.Certificate = None): + ''' + test basic p12 certificate bundle assumptions + + Args: + cdir (s_certdir.CertDir): certdir object + cert (crypto.X509): Cert to test + key (crypto.PKey): Key for the certification + p12 (crypto.PKCS12): PKCS12 object to test + cacert (crypto.X509): Corresponding CA cert (optional) + ''' + self.nn(p12) + + # Pull out the CA cert and keypair data + p12_cacert = None + if cacert: + p12_cacert = p12.additional_certs + self.nn(p12_cacert) + self.len(1, p12_cacert) + p12_cacert = p12_cacert[0].certificate + _pb = p12_cacert.public_bytes(c_serialization.Encoding.PEM) + _cb = cacert.public_bytes(c_serialization.Encoding.PEM) + self.eq(_cb, _pb) + + p12_cert = p12.cert.certificate + p12_key = p12.key + self.basic_assertions(cdir, p12_cert, p12_key, cacert=p12_cacert) + + # Make sure that the CA cert and keypair files are the same as the CA cert and keypair contained in the p12 file + _pb = p12_cert.public_bytes(c_serialization.Encoding.PEM) + _cb = cert.public_bytes(c_serialization.Encoding.PEM) + self.eq(_cb, _pb) + + _pb = p12_key.private_bytes(encoding=c_serialization.Encoding.PEM, + format=c_serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=c_serialization.NoEncryption()) + _cb = key.private_bytes(encoding=c_serialization.Encoding.PEM, + format=c_serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=c_serialization.NoEncryption()) + self.eq(_cb, _pb) + def test_certdir_cas(self): with self.getCertDir() as cdir: # type: s_certdir.CertDiNew @@ -272,6 +348,79 @@ def test_certdir_hosts(self): self.basic_assertions(cdir, cert, key, cacert=cacert) self.host_assertions(cdir, cert, key, cacert=cacert) + def test_certdir_users(self): + with self.getCertDir() as cdir: # type: s_certdir.CertDir + caname = 'syntest' + username = 'visi@vertex.link' + username_unsigned = 'unsigned@vertex.link' + base = cdir._getPathJoin() + + cdir.genCaCert(caname) + cacert = cdir.getCaCert(caname) + + # Test that all the methods for loading the certificates return correct values for non-existant files + self.none(cdir.getUserCert(username_unsigned)) + self.none(cdir.getUserKey(username_unsigned)) + self.none(cdir.getClientCert(username_unsigned)) + self.false(cdir.isUserCert(username_unsigned)) + self.false(cdir.isClientCert(username_unsigned)) + self.none(cdir.getUserCertPath('nope')) + self.none(cdir.getUserKeyPath('nope')) + self.none(cdir.getUserCaPath('nope')) + self.none(cdir.getUserForHost('nope', 'host.vertex.link')) + + # Generate a self-signed user keypair ============================= + cdir.genUserCert(username_unsigned) + self.raises(s_exc.NoSuchFile, cdir.genClientCert, username_unsigned) + + # Test that all the methods for loading the certificates work + self.isinstance(cdir.getUserCert(username_unsigned), c_x509.Certificate) + self.isinstance(cdir.getUserKey(username_unsigned), c_rsa.RSAPrivateKey) + self.none(cdir.getClientCert(username_unsigned)) + self.true(cdir.isUserCert(username_unsigned)) + self.false(cdir.isClientCert(username_unsigned)) + self.eq(cdir.getUserCertPath(username_unsigned), base + '/users/' + username_unsigned + '.crt') + self.eq(cdir.getUserKeyPath(username_unsigned), base + '/users/' + username_unsigned + '.key') + self.none(cdir.getUserCaPath(username_unsigned)) # no CA + self.eq(cdir.getUserForHost('unsigned', 'host.vertex.link'), username_unsigned) + + # Run basic assertions on the host keypair + cert = cdir.getUserCert(username_unsigned) + key = cdir.getUserKey(username_unsigned) + self.basic_assertions(cdir, cert, key) + self.user_assertions(cdir, cert, key) + + # Generate a signed user keypair ================================== + cdir.genUserCert(username, signas=caname) + cdir.genClientCert(username) + + # Test that all the methods for loading the certificates work + self.isinstance(cdir.getUserCert(username), c_x509.Certificate) + self.isinstance(cdir.getUserKey(username), c_rsa.RSAPrivateKey) + self.isinstance(cdir.getClientCert(username), c_pkcs12.PKCS12KeyAndCertificates) + self.true(cdir.isUserCert(username)) + self.true(cdir.isClientCert(username)) + self.eq(cdir.getUserCertPath(username), base + '/users/' + username + '.crt') + self.eq(cdir.getUserKeyPath(username), base + '/users/' + username + '.key') + self.eq(cdir.getUserCaPath(username), base + '/cas/' + caname + '.crt') + self.eq(cdir.getUserForHost('visi', 'host.vertex.link'), username) + + # Run basic assertions on the host keypair + cert = cdir.getUserCert(username) + key = cdir.getUserKey(username) + p12 = cdir.getClientCert(username) + self.basic_assertions(cdir, cert, key, cacert=cacert) + self.user_assertions(cdir, cert, key, cacert=cacert) + self.p12_assertions(cdir, cert, key, p12, cacert=cacert) + + # Test missing files for generating a client cert + os.remove(base + '/users/' + username + '.key') + self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user key + os.remove(base + '/cas/' + caname + '.crt') + self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # ca crt + os.remove(base + '/users/' + username + '.crt') + self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user crt + class CertDirTest(s_t_utils.SynTest): From 900e8e08618f8425546a4a4cb5425e538f7528b1 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Mon, 12 Feb 2024 18:30:26 +0000 Subject: [PATCH 06/31] Add CSR support --- synapse/lib/certdir.py | 741 +++++++++++++++++------------- synapse/tests/test_lib_certdir.py | 175 +++++++ 2 files changed, 585 insertions(+), 331 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 9f46a1f3dd..25e5c0b3e5 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -21,6 +21,7 @@ import cryptography.hazmat.primitives.hashes as c_hashes import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa +import cryptography.hazmat.primitives.asymmetric.types as c_types import cryptography.hazmat.primitives.serialization as c_serialization import cryptography.hazmat.primitives.serialization.pkcs12 as c_pkcs12 @@ -36,12 +37,17 @@ StrOrNoneType = Union[AnyStr | None] BytesOrNoneType = Union[ByteString | None] +OutPutOrNoneType = Union[s_output.OutPut | None] CertOrNoneType = Union[c_x509.Certificate | None] PkeyOrNoneType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey | None] PkeyAndCertType = Tuple[c_rsa.RSAPrivateKey, c_x509.Certificate] PkeyAndBuilderType = Tuple[c_rsa.RSAPrivateKey, c_x509.CertificateBuilder] +PrivKeyPubKeyBuilderType = Tuple[Union[c_rsa.RSAPrivateKey | None], c_types.PublicKeyTypes, c_x509.CertificateBuilder] PkeyType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey] Pkcs12OrNoneType = Union[c_pkcs12.PKCS12KeyAndCertificates | None] +PkeyOrNoneAndCertType = Tuple[Union[c_rsa.RSAPrivateKey | None], c_x509.Certificate] +# Used for handling CSRs +PubKeyOrNone = Union[c_types.PublicKeyTypes | None] def iterFqdnUp(fqdn): levs = fqdn.split('.') @@ -157,6 +163,71 @@ def _save(self, timestamp=None): fd.truncate(0) fd.write(crypto.dump_crl(crypto.FILETYPE_PEM, self.opensslcrl)) +class CRLNew: + + def __init__(self, certdir: CertDirNew, name: AnyStr): + + self.name = name + self.certdir = certdir + self.path = certdir.genCrlPath(name) + + if os.path.isfile(self.path): + with io.open(self.path, 'rb') as fd: + self.opensslcrl = crypto.load_crl(crypto.FILETYPE_PEM, fd.read()) + + else: + self.opensslcrl = crypto.CRL() + + def revoke(self, cert): + ''' + Revoke a certificate with the CRL. + + Args: + cert (cryto.X509): The certificate to revoke. + + Returns: + None + ''' + try: + self._verify(cert) + except s_exc.BadCertVerify as e: + raise s_exc.BadCertVerify(mesg=f'Failed to validate that certificate was signed by {self.name}') from e + timestamp = time.strftime('%Y%m%d%H%M%SZ').encode() + revoked = crypto.Revoked() + revoked.set_reason(None) + revoked.set_rev_date(timestamp) + revoked.set_serial(b'%x' % cert.get_serial_number()) + + self.opensslcrl.add_revoked(revoked) + self._save(timestamp) + + def _verify(self, cert): + # Verify the cert was signed by the CA in self.name + cacert = self.certdir.getCaCert(self.name) + store = crypto.X509Store() + store.add_cert(cacert) + store.set_flags(crypto.X509StoreFlags.PARTIAL_CHAIN) + ctx = crypto.X509StoreContext(store, cert,) + try: + ctx.verify_certificate() + except crypto.X509StoreContextError as e: + raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) from None + + def _save(self, timestamp=None): + + if timestamp is None: + timestamp = time.strftime('%Y%m%d%H%M%SZ').encode() + + pkey = self.certdir.getCaKey(self.name) + cert = self.certdir.getCaCert(self.name) + + self.opensslcrl.set_lastUpdate(timestamp) + self.opensslcrl.sign(cert, pkey, b'sha256') + + with s_common.genfile(self.path) as fd: + fd.truncate(0) + fd.write(crypto.dump_crl(crypto.FILETYPE_PEM, self.opensslcrl)) + def getServerSSLContext() -> ssl.SSLContext: ''' Get a server SSLContext object. @@ -1565,7 +1636,7 @@ def delCertPath(self, *path): def genCaCert(self, name: AnyStr, signas: Optional[AnyStr | None] = None, - outp: s_output.OutPut =None, + outp: OutPutOrNoneType =None, save: bool =True) -> PkeyAndCertType: ''' Generates a CA keypair. @@ -1585,7 +1656,8 @@ def genCaCert(self, name: AnyStr, #XXX FIX PkeyAndCertType: + save: bool = True) -> PkeyOrNoneAndCertType: ''' Generates a host keypair. @@ -1631,7 +1703,14 @@ def genHostCert(self, name: AnyStr, Returns: ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the private key and certificate objects. ''' - pkey, builder = self._genBasePkeyCert(name, pkey=csr) + if csr is None: + prvkey = self._genPrivKey() + pubkey = prvkey.public_key() + else: + prvkey = None + pubkey = csr + + builder = self._genCertBuilder(name, pubkey) # XXX FIXME - Sort out some generic SANS support from pyopenssl loose apis # ext_sans = {'DNS:' + name} @@ -1658,49 +1737,44 @@ def genHostCert(self, name: AnyStr, if signas is not None: cert = self.signCertAs(builder, signas) else: - cert = self.selfSignCert(builder, pkey) + cert = self.selfSignCert(builder, prvkey) if save: - # XXX FIXME - This code refers to a private member of the old openssl Pkey class :( - # Lets let this fly until its a problem. - # if not pkey._only_public: - # keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) - # if outp is not None: - # outp.printf('key saved: %s' % (keypath,)) - keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) + if prvkey is not None: + keypath = self._savePkeyTo(prvkey, 'hosts', '%s.key' % name) + if outp is not None: + outp.printf('key saved: %s' % (keypath,)) crtpath = self._saveCertTo(cert, 'hosts', '%s.crt' % name) if outp is not None: outp.printf('cert saved: %s' % (crtpath,)) - return pkey, cert + return prvkey, cert + + def genHostCsr(self, name: AnyStr, outp: OutPutOrNoneType =None) -> ByteString: + ''' + Generates a host certificate signing request. + + Args: + name (str): The name of the host CSR. + outp (synapse.lib.output.Output): The output buffer. + + Examples: + Generate a CSR for the host key named "myhost": + + cdir.genHostCsr('myhost') + + Returns: + bytes: The bytes of the CSR. + ''' + return self._genPkeyCsr(name, 'hosts', outp=outp) - # def genHostCsr(self, name, outp=None): - # ''' - # Generates a host certificate signing request. - # - # Args: - # name (str): The name of the host CSR. - # outp (synapse.lib.output.Output): The output buffer. - # - # Examples: - # Generate a CSR for the host key named "myhost": - # - # cdir.genHostCsr('myhost') - # - # Returns: - # bytes: The bytes of the CSR. - # ''' - # return self._genPkeyCsr(name, 'hosts', outp=outp) - # def genUserCert(self, name: AnyStr, signas: Optional[AnyStr | None] = None, - outp: s_output.OutPut = None, - csr: PkeyOrNoneType =None, - save: bool = True) -> PkeyAndCertType: + outp: OutPutOrNoneType = None, + csr: PubKeyOrNone =None, + save: bool = True) -> PkeyOrNoneAndCertType: ''' Generates a user keypair. @@ -1718,7 +1792,14 @@ def genUserCert(self, Returns: ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. ''' - pkey, builder = self._genBasePkeyCert(name, pkey=csr) + if csr is None: + prvkey = self._genPrivKey() + pubkey = prvkey.public_key() + else: + prvkey = None + pubkey = csr + + builder = self._genCertBuilder(name, pubkey) builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) builder = builder.add_extension( @@ -1739,24 +1820,19 @@ def genUserCert(self, if signas is not None: cert = self.signCertAs(builder, signas) else: - cert = self.selfSignCert(builder, pkey) + cert = self.selfSignCert(builder, prvkey) if save: - # XXX FIXME - This code refers to a private member of the old openssl Pkey class :( - # Lets let this fly until its a problem. - # if not pkey._only_public: - # keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) - # if outp is not None: - # outp.printf('key saved: %s' % (keypath,)) - keypath = self._savePkeyTo(pkey, 'users', '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) + if prvkey is not None: + keypath = self._savePkeyTo(prvkey, 'users', '%s.key' % name) + if outp is not None: + outp.printf('key saved: %s' % (keypath,)) crtpath = self._saveCertTo(cert, 'users', '%s.crt' % name) if outp is not None: outp.printf('cert saved: %s' % (crtpath,)) - return pkey, cert + return prvkey, cert # # def genCodeCert(self, name, signas=None, outp=None, save=True): @@ -1894,7 +1970,7 @@ def genUserCert(self, # # return crls # - def genClientCert(self, name: AnyStr, outp: s_output.OutPut =None) -> None: + def genClientCert(self, name: AnyStr, outp: OutPutOrNoneType =None) -> None: ''' Generates a user PKCS #12 archive. Please note that the resulting file will contain private key material. @@ -1932,54 +2008,54 @@ def genClientCert(self, name: AnyStr, outp: s_output.OutPut =None) -> None: crtpath = self._saveP12To(byts, 'users', '%s.p12' % name) if outp is not None: outp.printf('client cert saved: %s' % (crtpath,)) - # - # def valUserCert(self, byts, cacerts=None): - # ''' - # Validate the PEM encoded x509 user certificate bytes and return it. - # - # Args: - # byts (bytes): The bytes for the User Certificate. - # cacerts (tuple): A tuple of OpenSSL.crypto.X509 CA Certificates. - # - # Raises: - # BadCertVerify: If the certificate is not valid. - # - # Returns: - # OpenSSL.crypto.X509: The certificate, if it is valid. - # ''' - # cert = self.loadCertByts(byts) - # - # if cacerts is None: - # cacerts = self.getCaCerts() - # - # store = crypto.X509Store() - # [store.add_cert(cacert) for cacert in cacerts] - # - # ctx = crypto.X509StoreContext(store, cert) - # try: - # ctx.verify_certificate() - # except crypto.X509StoreContextError as e: - # raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) - # return cert - # - # def genUserCsr(self, name, outp=None): - # ''' - # Generates a user certificate signing request. - # - # Args: - # name (str): The name of the user CSR. - # outp (synapse.lib.output.Output): The output buffer. - # - # Examples: - # Generate a CSR for the user "myuser": - # - # cdir.genUserCsr('myuser') - # - # Returns: - # bytes: The bytes of the CSR. - # ''' - # return self._genPkeyCsr(name, 'users', outp=outp) - # + + def valUserCert(self, byts: ByteString, cacerts: Union[List[c_x509.Certificate] | None] =None): + ''' + Validate the PEM encoded x509 user certificate bytes and return it. + + Args: + byts (bytes): The bytes for the User Certificate. + cacerts (tuple): A tuple of OpenSSL.crypto.X509 CA Certificates. + + Raises: + BadCertVerify: If the certificate is not valid. + + Returns: + OpenSSL.crypto.X509: The certificate, if it is valid. + ''' + cert = self.loadCertByts(byts) + + if cacerts is None: + cacerts = self.getCaCerts() + + store = crypto.X509Store() + [store.add_cert(crypto.X509.from_cryptography(cacert)) for cacert in cacerts] + + ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert)) + try: + ctx.verify_certificate() + except crypto.X509StoreContextError as e: + raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) + return cert + + def genUserCsr(self, name: AnyStr, outp: OutPutOrNoneType =None) -> ByteString: + ''' + Generates a user certificate signing request. + + Args: + name (str): The name of the user CSR. + outp (synapse.lib.output.Output): The output buffer. + + Examples: + Generate a CSR for the user "myuser": + + cdir.genUserCsr('myuser') + + Returns: + bytes: The bytes of the CSR. + ''' + return self._genPkeyCsr(name, 'users', outp=outp) + def getCaCert(self, name): ''' Loads the X509 object for a given CA. @@ -1997,38 +2073,38 @@ def getCaCert(self, name): : The certificate, if exists. ''' return self._loadCertPath(self.getCaCertPath(name)) - # - # def getCaCertBytes(self, name): - # path = self.getCaCertPath(name) - # if os.path.exists(path): - # with open(path, 'rb') as fd: - # return fd.read() - # - # def getCaCerts(self): - # ''' - # Return a list of CA certs from the CertDir. - # - # Returns: - # [OpenSSL.crypto.X509]: List of CA certificates. - # ''' - # retn = [] - # - # for cdir in self.certdirs: - # - # path = s_common.genpath(cdir, 'cas') - # if not os.path.isdir(path): - # continue - # - # for name in os.listdir(path): - # - # if not name.endswith('.crt'): # pragma: no cover - # continue - # - # full = s_common.genpath(cdir, 'cas', name) - # retn.append(self._loadCertPath(full)) - # - # return retn - # + + def getCaCertBytes(self, name: AnyStr) -> ByteString: + path = self.getCaCertPath(name) + if os.path.exists(path): + with open(path, 'rb') as fd: + return fd.read() + + def getCaCerts(self) -> List[c_x509.Certificate]: + ''' + Return a list of CA certs from the CertDir. + + Returns: + [OpenSSL.crypto.X509]: List of CA certificates. + ''' + retn = [] + + for cdir in self.certdirs: + + path = s_common.genpath(cdir, 'cas') + if not os.path.isdir(path): + continue + + for name in os.listdir(path): + + if not name.endswith('.crt'): # pragma: no cover + continue + + full = s_common.genpath(cdir, 'cas', name) + retn.append(self._loadCertPath(full)) + + return retn + def getCaCertPath(self, name: AnyStr) -> StrOrNoneType: ''' Gets the path to a CA certificate. @@ -2356,46 +2432,46 @@ def getUserKeyPath(self, name: AnyStr) -> StrOrNoneType: # path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) # if os.path.isfile(path): # return path - # def importFile(self, path, mode, outp=None): - # ''' - # Imports certs and keys into the Synapse cert directory - # - # Args: - # path (str): The path of the file to be imported. - # mode (str): The certdir subdirectory to import the file into. - # - # Examples: - # Import CA certifciate 'mycoolca.crt' to the 'cas' directory. - # - # certdir.importFile('mycoolca.crt', 'cas') - # - # Notes: - # importFile does not perform any validation on the files it imports. - # - # Returns: - # None - # ''' - # if not os.path.isfile(path): - # raise s_exc.NoSuchFile(mesg=f'File {path} does not exist', path=path) - # - # fname = os.path.split(path)[1] - # parts = fname.rsplit('.', 1) - # ext = parts[1] if len(parts) == 2 else None - # - # if not ext or ext not in ('crt', 'key', 'p12'): - # mesg = 'importFile only supports .crt, .key, .p12 extensions' - # raise s_exc.BadFileExt(mesg=mesg, ext=ext) - # - # newpath = s_common.genpath(self.certdirs[0], mode, fname) - # if os.path.isfile(newpath): - # raise s_exc.FileExists(mesg=f'File {newpath} already exists', path=path) - # - # s_common.gendir(os.path.dirname(newpath)) - # - # shutil.copy(path, newpath) - # if outp is not None: - # outp.printf('copied %s to %s' % (path, newpath)) - # + def importFile(self, path: AnyStr, mode: AnyStr, outp: OutPutOrNoneType =None) -> None: + ''' + Imports certs and keys into the Synapse cert directory + + Args: + path (str): The path of the file to be imported. + mode (str): The certdir subdirectory to import the file into. + + Examples: + Import CA certifciate 'mycoolca.crt' to the 'cas' directory. + + certdir.importFile('mycoolca.crt', 'cas') + + Notes: + importFile does not perform any validation on the files it imports. + + Returns: + None + ''' + if not os.path.isfile(path): + raise s_exc.NoSuchFile(mesg=f'File {path} does not exist', path=path) + + fname = os.path.split(path)[1] + parts = fname.rsplit('.', 1) + ext = parts[1] if len(parts) == 2 else None + + if not ext or ext not in ('crt', 'key', 'p12'): + mesg = 'importFile only supports .crt, .key, .p12 extensions' + raise s_exc.BadFileExt(mesg=mesg, ext=ext) + + newpath = s_common.genpath(self.certdirs[0], mode, fname) + if os.path.isfile(newpath): + raise s_exc.FileExists(mesg=f'File {newpath} already exists', path=path) + + s_common.gendir(os.path.dirname(newpath)) + + shutil.copy(path, newpath) + if outp is not None: + outp.printf('copied %s to %s' % (path, newpath)) + def isCaCert(self, name: AnyStr) -> bool: ''' Checks if a CA certificate exists. @@ -2498,29 +2574,34 @@ def signCertAs(self, builder: c_x509.CertificateBuilder, signas: AnyStr) -> c_x5 private_key=cakey, algorithm=self.signing_digest(), ) return certificate - # - # def signHostCsr(self, xcsr, signas, outp=None, sans=None, save=True): - # ''' - # Signs a host CSR with a CA keypair. - # - # Args: - # xcsr (OpenSSL.crypto.X509Req): The certificate signing request. - # signas (str): The CA keypair name to sign the CSR with. - # outp (synapse.lib.output.Output): The output buffer. - # sans (list): List of subject alternative names. - # - # Examples: - # Sign a host key with the CA "myca": - # - # cdir.signHostCsr(mycsr, 'myca') - # - # Returns: - # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. - # ''' - # pkey = xcsr.get_pubkey() - # name = xcsr.get_subject().CN - # return self.genHostCert(name, csr=pkey, signas=signas, outp=outp, sans=sans, save=save) - # + + def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, + signas: AnyStr, + outp: OutPutOrNoneType=None, + sans=None, + save: bool =True) -> PkeyOrNoneAndCertType: + ''' + Signs a host CSR with a CA keypair. + + Args: + xcsr (OpenSSL.crypto.X509Req): The certificate signing request. + signas (str): The CA keypair name to sign the CSR with. + outp (synapse.lib.output.Output): The output buffer. + sans (list): List of subject alternative names. + + Examples: + Sign a host key with the CA "myca": + + cdir.signHostCsr(mycsr, 'myca') + + Returns: + ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. + ''' + pkey = xcsr.public_key() + name = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = name.value + return self.genHostCert(name, csr=pkey, signas=signas, outp=outp, sans=sans, save=save) + def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_x509.Certificate: ''' Self-sign a certificate. @@ -2547,77 +2628,80 @@ def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_ ) return certificate - # - # def signUserCsr(self, xcsr, signas, outp=None, save=True): - # ''' - # Signs a user CSR with a CA keypair. - # - # Args: - # xcsr (OpenSSL.crypto.X509Req): The certificate signing request. - # signas (str): The CA keypair name to sign the CSR with. - # outp (synapse.lib.output.Output): The output buffer. - # - # Examples: - # cdir.signUserCsr(mycsr, 'myca') - # - # Returns: - # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. - # ''' - # pkey = xcsr.get_pubkey() - # name = xcsr.get_subject().CN - # return self.genUserCert(name, csr=pkey, signas=signas, outp=outp, save=save) - # - # def _loadCasIntoSSLContext(self, ctx): - # - # for cdir in self.certdirs: - # - # path = s_common.genpath(cdir, 'cas') - # if not os.path.isdir(path): - # continue - # - # for name in os.listdir(path): - # if name.endswith('.crt'): - # ctx.load_verify_locations(os.path.join(path, name)) - # - # def getClientSSLContext(self, certname=None): - # ''' - # Returns an ssl.SSLContext appropriate for initiating a TLS session - # - # Args: - # certname: If specified, use the user certificate with the matching - # name to authenticate to the remote service. - # Returns: - # ssl.SSLContext: A SSLContext object. - # ''' - # sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) - # sslctx.minimum_version = ssl.TLSVersion.TLSv1_2 - # self._loadCasIntoSSLContext(sslctx) - # - # if certname is not None: - # - # username = certname - # if username.find('@') != -1: - # user, host = username.split('@', 1) - # username = self.getUserForHost(user, host) - # - # if username is None: - # mesg = f'User certificate not found: {certname}' - # raise s_exc.NoSuchCert(mesg=mesg) - # - # certpath = self.getUserCertPath(username) - # if certpath is None: - # mesg = f'User certificate not found: {certname}' - # raise s_exc.NoSuchCert(mesg=mesg) - # - # keypath = self.getUserKeyPath(username) - # if keypath is None: - # mesg = f'User private key not found: {certname}' - # raise s_exc.NoCertKey(mesg=mesg) - # - # sslctx.load_cert_chain(certpath, keypath) - # - # return sslctx - # + def signUserCsr(self, xcsr: c_x509.CertificateSigningRequest, + signas: AnyStr, + outp: OutPutOrNoneType=None, + save: bool =True) -> PkeyOrNoneAndCertType: + ''' + Signs a user CSR with a CA keypair. + + Args: + xcsr (OpenSSL.crypto.X509Req): The certificate signing request. + signas (str): The CA keypair name to sign the CSR with. + outp (synapse.lib.output.Output): The output buffer. + + Examples: + cdir.signUserCsr(mycsr, 'myca') + + Returns: + ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. + ''' + pkey = xcsr.public_key() + name = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = name.value + return self.genUserCert(name, csr=pkey, signas=signas, outp=outp, save=save) + + def _loadCasIntoSSLContext(self, ctx): + + for cdir in self.certdirs: + + path = s_common.genpath(cdir, 'cas') + if not os.path.isdir(path): + continue + + for name in os.listdir(path): + if name.endswith('.crt'): + ctx.load_verify_locations(os.path.join(path, name)) + + def getClientSSLContext(self, certname: StrOrNoneType =None) -> ssl.SSLContext: + ''' + Returns an ssl.SSLContext appropriate for initiating a TLS session + + Args: + certname: If specified, use the user certificate with the matching + name to authenticate to the remote service. + Returns: + ssl.SSLContext: A SSLContext object. + ''' + sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + sslctx.minimum_version = ssl.TLSVersion.TLSv1_2 + self._loadCasIntoSSLContext(sslctx) + + if certname is not None: + + username = certname + if username.find('@') != -1: + user, host = username.split('@', 1) + username = self.getUserForHost(user, host) + + if username is None: + mesg = f'User certificate not found: {certname}' + raise s_exc.NoSuchCert(mesg=mesg) + + certpath = self.getUserCertPath(username) + if certpath is None: + mesg = f'User certificate not found: {certname}' + raise s_exc.NoSuchCert(mesg=mesg) + + keypath = self.getUserKeyPath(username) + if keypath is None: + mesg = f'User private key not found: {certname}' + raise s_exc.NoCertKey(mesg=mesg) + + sslctx.load_cert_chain(certpath, keypath) + + return sslctx + # def getServerSSLContext(self, hostname=None, caname=None): # ''' # Returns an ssl.SSLContext appropriate to listen on a socket @@ -2735,52 +2819,48 @@ def _checkDupFile(self, path) -> None: if os.path.isfile(path): raise s_exc.DupFileName(mesg=f'Duplicate file {path}', path=path) - def _genBasePkeyCert(self, name: AnyStr, pkey: PkeyOrNoneType =None) -> PkeyAndBuilderType: - - if pkey is None: - pkey = c_rsa.generate_private_key(65537, self.crypto_numbits) + def _genPrivKey(self) -> c_rsa.RSAPrivateKey: + return c_rsa.generate_private_key(65537, self.crypto_numbits) + def _genCertBuilder(self, name: AnyStr, pubkey: c_types.PublicKeyTypes) -> c_x509.CertificateBuilder: builder = c_x509.CertificateBuilder() builder = builder.subject_name(c_x509.Name([ c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), ])) now = datetime.datetime.utcnow() - - builder = builder.not_valid_before(datetime.datetime.utcnow()) - builder = builder.not_valid_after(now + TEN_YEARS_TD) # Certpairs are good for 10 years + builder = builder.not_valid_before(now) + builder = builder.not_valid_after(now + TEN_YEARS_TD) # certificates are good for 10 years builder = builder.serial_number(int(s_common.guid(), 16)) - builder = builder.public_key(pkey.public_key()) - return pkey, builder - # - # def _genPkeyCsr(self, name, mode, outp=None): - # pkey = crypto.PKey() - # pkey.generate_key(crypto.TYPE_RSA, self.crypto_numbits) - # - # xcsr = crypto.X509Req() - # xcsr.get_subject().CN = name - # - # xcsr.set_pubkey(pkey) - # xcsr.sign(pkey, self.signing_digest) - # - # keypath = self._savePkeyTo(pkey, mode, '%s.key' % name) - # if outp is not None: - # outp.printf('key saved: %s' % (keypath,)) - # - # csrpath = self._getPathJoin(mode, '%s.csr' % name) - # self._checkDupFile(csrpath) - # - # byts = crypto.dump_certificate_request(crypto.FILETYPE_PEM, xcsr) - # - # with s_common.genfile(csrpath) as fd: - # fd.truncate(0) - # fd.write(byts) - # - # if outp is not None: - # outp.printf('csr saved: %s' % (csrpath,)) - # - # return byts - # + builder = builder.public_key(pubkey) + return builder + + def _genPkeyCsr(self, name: AnyStr, mode: AnyStr, outp: OutPutOrNoneType=None) -> ByteString: + + pkey = self._genPrivKey() + + builder = c_x509.CertificateSigningRequestBuilder() + builder = builder.subject_name(c_x509.Name([c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name),])) + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=True) + request = builder.sign(pkey, c_hashes.SHA256()) + + keypath = self._savePkeyTo(pkey, mode, '%s.key' % name) + if outp is not None: + outp.printf('key saved: %s' % (keypath,)) + + csrpath = self._getPathJoin(mode, '%s.csr' % name) + self._checkDupFile(csrpath) + byts = request.public_bytes(c_serialization.Encoding.PEM) + + with s_common.genfile(csrpath) as fd: + fd.truncate(0) + fd.write(byts) + + if outp is not None: + outp.printf('csr saved: %s' % (csrpath,)) + + return byts + def _getCaPath(self, cert: c_x509.Certificate) -> StrOrNoneType: issuer = cert.issuer.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] return self.getCaCertPath(issuer.value) @@ -2799,28 +2879,27 @@ def _loadCertPath(self, path: AnyStr) -> CertOrNoneType: if byts: return self._loadCertByts(byts) - # def loadCertByts(self, byts): - # ''' - # Load a X509 certificate from its PEM encoded bytes. - # - # Args: - # byts (bytes): The PEM encoded bytes of the certificate. - # - # Returns: - # OpenSSL.crypto.X509: The X509 certificate. - # - # Raises: - # BadCertBytes: If the certificate bytes are invalid. - # ''' - # return self._loadCertByts(byts) - # + def loadCertByts(self, byts: ByteString) -> c_x509.Certificate: + ''' + Load a X509 certificate from its PEM encoded bytes. + + Args: + byts (bytes): The PEM encoded bytes of the certificate. + + Returns: + OpenSSL.crypto.X509: The X509 certificate. + + Raises: + BadCertBytes: If the certificate bytes are invalid. + ''' + return self._loadCertByts(byts) + def _loadCertByts(self, byts: bytes) -> c_x509.Certificate: try: return c_x509.load_pem_x509_certificate(byts) except Exception as e: logger.exception('UNKNOWN EXCEPTION READING BYTES!') - # FIXME - raise raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') - raise + raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {e}') from None # except crypto.Error as e: # # Unwrap pyopenssl's exception_from_error_queue @@ -2831,14 +2910,14 @@ def _loadCertByts(self, byts: bytes) -> c_x509.Certificate: # estr += ' '.join((arg for arg in argv[0] if arg)) # raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') - # def _loadCsrPath(self, path): - # byts = self._getPathBytes(path) - # if byts: - # return self._loadCsrByts(byts) - # - # def _loadCsrByts(self, byts): - # return crypto.load_certificate_request(crypto.FILETYPE_PEM, byts) - # + def _loadCsrPath(self, path: AnyStr) -> Union[c_x509.CertificateSigningRequest | None]: + byts = self._getPathBytes(path) + if byts: + return self._loadCsrByts(byts) + + def _loadCsrByts(self, byts) -> ByteString: + return c_x509.load_pem_x509_csr(byts) + def _loadKeyPath(self, path: AnyStr) -> PkeyOrNoneType: byts = self._getPathBytes(path) if byts: diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 74cb099c27..87729f1d56 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -421,6 +421,181 @@ def test_certdir_users(self): os.remove(base + '/users/' + username + '.crt') self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user crt + def test_certdir_hosts_sans(self): + self.skip('XXX FIXME Sort out SANS support.') + with self.getCertDir() as cdir: # type: s_certdir.CertDir + caname = 'syntest' + cdir.genCaCert(caname) + + # Host cert with multiple SANs ==================================== + hostname = 'visi.vertex.link' + sans = 'DNS:vertex.link,DNS:visi.vertex.link,DNS:vertex.link' + cdir.genHostCert(hostname, signas=caname, sans=sans) + + cdir.getCaCert(caname) + cert = cdir.getHostCert(hostname) + cdir.getHostKey(hostname) + + self.eq(cert.get_extension_count(), 5) + self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') + self.eq(cert.get_extension(4).get_data(), b'0\x1f\x82\x0bvertex.link\x82\x10visi.vertex.link') # ASN.1 encoded subjectAltName data + + # Host cert with no specified SANs ================================ + hostname = 'visi2.vertex.link' + cdir.genHostCert(hostname, signas=caname) + + cdir.getCaCert(caname) + cert = cdir.getHostCert(hostname) + cdir.getHostKey(hostname) + + self.eq(cert.get_extension_count(), 5) + self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') + self.eq(cert.get_extension(4).get_data(), b'0\x13\x82\x11visi2.vertex.link') # ASN.1 encoded subjectAltName data + + # Self-signed Host cert with no specified SANs ==================== + hostname = 'visi3.vertex.link' + cdir.genHostCert(hostname) + + cdir.getCaCert(caname) + cert = cdir.getHostCert(hostname) + cdir.getHostKey(hostname) + + self.eq(cert.get_extension_count(), 5) + self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') + self.eq(cert.get_extension(4).get_data(), b'0\x13\x82\x11visi3.vertex.link') # ASN.1 encoded subjectAltName data + + def test_certdir_hosts_csr(self): + with self.getCertDir() as cdir: # type: s_certdir.CertDir + caname = 'syntest' + hostname = 'visi.vertex.link' + + # Generate CA cert and host CSR + cdir.genCaCert(caname) + cdir.genHostCsr(hostname) + path = cdir._getPathJoin('hosts', hostname + '.csr') + xcsr = cdir._loadCsrPath(path) + + # Sign the CSR as the CA + pkey, pcert = cdir.signHostCsr(xcsr, caname) + self.none(pkey) + self.isinstance(pcert, c_x509.Certificate) + + # Validate the keypair + cacert = cdir.getCaCert(caname) + cert = cdir.getHostCert(hostname) + key = cdir.getHostKey(hostname) + self.basic_assertions(cdir, cert, key, cacert=cacert) + + def test_certdir_users_csr(self): + with self.getCertDir() as cdir: # type: s_certdir.CertDir + caname = 'syntest' + username = 'visi@vertex.link' + + # Generate CA cert and user CSR + cdir.genCaCert(caname) + cdir.genUserCsr(username) + path = cdir._getPathJoin('users', username + '.csr') + xcsr = cdir._loadCsrPath(path) + + # Sign the CSR as the CA + pkey, pcert = cdir.signUserCsr(xcsr, caname) + self.none(pkey) + self.isinstance(pcert, c_x509.Certificate) + + # Validate the keypair + cacert = cdir.getCaCert(caname) + cert = cdir.getUserCert(username) + key = cdir.getUserKey(username) + self.basic_assertions(cdir, cert, key, cacert=cacert) + + def test_certdir_importfile(self): + with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getTestDir() as testpath: + + # File doesn't exist + fpath = s_common.genpath(testpath, 'not_real.crt') + self.raises(s_exc.NoSuchFile, cdir.importFile, fpath, 'cas') + + # File has unsupported extension + fpath = s_common.genpath(testpath, 'coolpic.bmp') + with s_common.genfile(fpath) as fd: + self.raises(s_exc.BadFileExt, cdir.importFile, fpath, 'cas') + + tests = ( + ('cas', 'coolca.crt'), + ('cas', 'coolca.key'), + ('hosts', 'coolhost.crt'), + ('hosts', 'coolhost.key'), + ('users', 'cooluser.crt'), + ('users', 'cooluser.key'), + ('users', 'cooluser.p12'), + ) + for ftype, fname in tests: + srcpath = s_common.genpath(testpath, fname) + dstpath = s_common.genpath(cdir.certdirs[0], ftype, fname) + + with s_common.genfile(srcpath) as fd: + fd.write(b'arbitrary data') + fd.seek(0) + + # Make sure the file is not there + self.raises(s_exc.NoSuchFile, s_common.reqfile, dstpath) + + # Import it and make sure it exists + self.none(cdir.importFile(srcpath, ftype)) + with s_common.reqfile(dstpath) as dstfd: + self.eq(dstfd.read(), b'arbitrary data') + + # Make sure it can't be overwritten + self.raises(s_exc.FileExists, cdir.importFile, srcpath, ftype) + + def test_certdir_valUserCert(self): + with self.getCertDir() as cdir: # type: s_certdir.CertDir + cdir._getPathJoin() + cdir.genCaCert('syntest') + cdir.genCaCert('newp') + cdir.getCaCerts() + syntestca = cdir.getCaCert('syntest') + newpca = cdir.getCaCert('newp') + + with self.raises(s_exc.BadCertBytes): + cdir.valUserCert(b'') + + cdir.genUserCert('cool') + path = cdir.getUserCertPath('cool') + byts = cdir._getPathBytes(path) + + self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts) + + cdir.genUserCert('cooler', signas='syntest') + path = cdir.getUserCertPath('cooler') + byts = cdir._getPathBytes(path) + self.nn(cdir.valUserCert(byts)) + self.nn(cdir.valUserCert(byts, cacerts=(syntestca,))) + self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=(newpca,)) + self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=()) + + cdir.genUserCert('coolest', signas='newp') + path = cdir.getUserCertPath('coolest') + byts = cdir._getPathBytes(path) + self.nn(cdir.valUserCert(byts)) + self.nn(cdir.valUserCert(byts, cacerts=(newpca,))) + self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=(syntestca,)) + self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=()) + + def test_certdir_sslctx(self): + + with self.getCertDir() as cdir: + + with self.raises(s_exc.NoSuchCert): + cdir.getClientSSLContext(certname='newp') + + with s_common.genfile(cdir.certdirs[0], 'users', 'newp.crt') as fd: + fd.write(b'asdf') + + with self.raises(s_exc.NoCertKey): + cdir.getClientSSLContext(certname='newp') + class CertDirTest(s_t_utils.SynTest): From fcf39e06857c3270ff057f02d051b2197e4ca5ed Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 13:31:29 +0000 Subject: [PATCH 07/31] Add codesign --- synapse/lib/certdir.py | 488 ++++++++++++++++-------------- synapse/lib/crypto/rsa.py | 6 +- synapse/tests/test_lib_certdir.py | 42 +++ 3 files changed, 299 insertions(+), 237 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 25e5c0b3e5..94fe9321ce 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -8,7 +8,7 @@ import datetime import collections -from typing import AnyStr, ByteString, List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union from OpenSSL import crypto # type: ignore @@ -35,8 +35,8 @@ TEN_YEARS = 10 * 365 * 24 * 60 * 60 # 10 years in seconds TEN_YEARS_TD = datetime.timedelta(seconds=TEN_YEARS) -StrOrNoneType = Union[AnyStr | None] -BytesOrNoneType = Union[ByteString | None] +StrOrNoneType = Union[str | None] +BytesOrNoneType = Union[bytes | None] OutPutOrNoneType = Union[s_output.OutPut | None] CertOrNoneType = Union[c_x509.Certificate | None] PkeyOrNoneType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey | None] @@ -165,20 +165,23 @@ def _save(self, timestamp=None): class CRLNew: - def __init__(self, certdir: CertDirNew, name: AnyStr): + def __init__(self, certdir, name): self.name = name self.certdir = certdir self.path = certdir.genCrlPath(name) + self.crlbuilder = c_x509.CertificateRevocationListBuilder().issuer_name(c_x509.Name([ + c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), + ])) + if os.path.isfile(self.path): with io.open(self.path, 'rb') as fd: - self.opensslcrl = crypto.load_crl(crypto.FILETYPE_PEM, fd.read()) - - else: - self.opensslcrl = crypto.CRL() + crl = c_x509.load_pem_x509_crl(fd.read()) + for revc in crl: + self.crlbuilder = self.crlbuilder.add_revoked_certificate(revc) - def revoke(self, cert): + def revoke(self, cert: c_x509.Certificate) -> None: ''' Revoke a certificate with the CRL. @@ -192,41 +195,43 @@ def revoke(self, cert): self._verify(cert) except s_exc.BadCertVerify as e: raise s_exc.BadCertVerify(mesg=f'Failed to validate that certificate was signed by {self.name}') from e - timestamp = time.strftime('%Y%m%d%H%M%SZ').encode() - revoked = crypto.Revoked() - revoked.set_reason(None) - revoked.set_rev_date(timestamp) - revoked.set_serial(b'%x' % cert.get_serial_number()) - self.opensslcrl.add_revoked(revoked) - self._save(timestamp) + now = datetime.datetime.utcnow() + builder = c_x509.RevokedCertificateBuilder() + builder = builder.serial_number(cert.serial_number) + builder = builder.revocation_date(now) + builder = builder.add_extension(c_x509.CRLReason(c_x509.ReasonFlags.unspecified), critical=False) + revoked_cert = builder.build() + + self.crlbuilder = self.crlbuilder.add_revoked_certificate(revoked_cert) + self._save(now) def _verify(self, cert): # Verify the cert was signed by the CA in self.name cacert = self.certdir.getCaCert(self.name) store = crypto.X509Store() - store.add_cert(cacert) + store.add_cert(crypto.X509.from_cryptography(cacert)) store.set_flags(crypto.X509StoreFlags.PARTIAL_CHAIN) - ctx = crypto.X509StoreContext(store, cert,) + ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert),) try: ctx.verify_certificate() except crypto.X509StoreContextError as e: raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) from None - def _save(self, timestamp=None): + def _save(self, timestamp: [datetime.datetime | None] =None) -> None: if timestamp is None: - timestamp = time.strftime('%Y%m%d%H%M%SZ').encode() + timestamp = datetime.datetime.utcnow() - pkey = self.certdir.getCaKey(self.name) - cert = self.certdir.getCaCert(self.name) - - self.opensslcrl.set_lastUpdate(timestamp) - self.opensslcrl.sign(cert, pkey, b'sha256') + self.crlbuilder = self.crlbuilder.last_update(timestamp) + # We have to have a next updated time; but we set it to be >= the lifespan of our certificates in general. + self.crlbuilder = self.crlbuilder.next_update(timestamp + TEN_YEARS_TD) + prvkey = self.certdir.getCaKey(self.name) + crl = self.crlbuilder.sign(private_key=prvkey, algorithm=c_hashes.SHA256()) with s_common.genfile(self.path) as fd: fd.truncate(0) - fd.write(crypto.dump_crl(crypto.FILETYPE_PEM, self.opensslcrl)) + fd.write(crl.public_bytes(c_serialization.Encoding.PEM)) def getServerSSLContext() -> ssl.SSLContext: ''' @@ -1634,8 +1639,8 @@ def delCertPath(self, *path): self.certdirs.remove(fullpath) self.pathrefs.pop(fullpath, None) - def genCaCert(self, name: AnyStr, - signas: Optional[AnyStr | None] = None, + def genCaCert(self, name: str, + signas: Optional[str | None] = None, outp: OutPutOrNoneType =None, save: bool =True) -> PkeyAndCertType: ''' @@ -1679,11 +1684,11 @@ def genCaCert(self, name: AnyStr, return prvkey, cert - def genHostCert(self, name: AnyStr, - signas: Optional[AnyStr | None] = None, + def genHostCert(self, name: str, + signas: Optional[str | None] = None, outp: OutPutOrNoneType = None, csr: PubKeyOrNone =None, - sans: List[AnyStr] =None, + sans: List[str] =None, save: bool = True) -> PkeyOrNoneAndCertType: ''' Generates a host keypair. @@ -1751,7 +1756,7 @@ def genHostCert(self, name: AnyStr, return prvkey, cert - def genHostCsr(self, name: AnyStr, outp: OutPutOrNoneType =None) -> ByteString: + def genHostCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: ''' Generates a host certificate signing request. @@ -1770,8 +1775,8 @@ def genHostCsr(self, name: AnyStr, outp: OutPutOrNoneType =None) -> ByteString: return self._genPkeyCsr(name, 'hosts', outp=outp) def genUserCert(self, - name: AnyStr, - signas: Optional[AnyStr | None] = None, + name: str, + signas: Optional[str | None] = None, outp: OutPutOrNoneType = None, csr: PubKeyOrNone =None, save: bool = True) -> PkeyOrNoneAndCertType: @@ -1834,143 +1839,149 @@ def genUserCert(self, return prvkey, cert - # - # def genCodeCert(self, name, signas=None, outp=None, save=True): - # ''' - # Generates a code signing keypair. - # - # Args: - # name (str): The name of the code signing cert. - # signas (str): The CA keypair to sign the new code keypair with. - # outp (synapse.lib.output.Output): The output buffer. - # - # Examples: - # - # Generate a code signing cert for the name "The Vertex Project": - # - # myuserkey, myusercert = cdir.genCodeCert('The Vertex Project') - # - # Returns: - # ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. - # ''' - # pkey, cert = self._genBasePkeyCert(name) - # - # cert.add_extensions([ - # crypto.X509Extension(b'nsCertType', False, b'objsign'), - # crypto.X509Extension(b'keyUsage', False, b'digitalSignature'), - # crypto.X509Extension(b'extendedKeyUsage', False, b'codeSigning'), - # crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE'), - # ]) - # - # if signas is not None: - # self.signCertAs(cert, signas) - # - # if save: - # crtpath = self._saveCertTo(cert, 'code', '%s.crt' % name) - # if outp is not None: - # outp.printf('cert saved: %s' % (crtpath,)) - # - # if not pkey._only_public: - # keypath = self._savePkeyTo(pkey, 'code', '%s.key' % name) - # if outp is not None: - # outp.printf('key saved: %s' % (keypath,)) - # - # return pkey, cert - # - # def getCodeKeyPath(self, name): - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'code', f'{name}.key') - # if os.path.isfile(path): - # return path - # - # def getCodeCertPath(self, name): - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'code', f'{name}.crt') - # if os.path.isfile(path): - # return path - # - # def getCodeKey(self, name): - # - # path = self.getCodeKeyPath(name) - # if path is None: - # return None - # - # pkey = self._loadKeyPath(path) - # return s_rsa.PriKey(pkey.to_cryptography_key()) - # - # def getCodeCert(self, name): - # - # path = self.getCodeCertPath(name) - # if path is None: # pragma: no cover - # return None - # - # return self._loadCertPath(path) - # - # def _getCertExt(self, cert, name): - # for i in range(cert.get_extension_count()): - # ext = cert.get_extension(i) - # if ext.get_short_name() == name: - # return ext.get_data() - # - # def valCodeCert(self, byts): - # ''' - # Verify a code cert is valid according to certdir's available CAs and CRLs. - # - # Args: - # byts (bytes): The certificate bytes. - # - # Returns: - # OpenSSL.crypto.X509: The certificate. - # ''' - # - # reqext = crypto.X509Extension(b'extendedKeyUsage', False, b'codeSigning') - # - # cert = self.loadCertByts(byts) - # if self._getCertExt(cert, b'extendedKeyUsage') != reqext.get_data(): - # mesg = 'Certificate is not for code signing.' - # raise s_exc.BadCertBytes(mesg=mesg) - # - # crls = self._getCaCrls() - # cacerts = self.getCaCerts() - # - # store = crypto.X509Store() - # [store.add_cert(cacert) for cacert in cacerts] - # - # if crls: - # - # store.set_flags(crypto.X509StoreFlags.CRL_CHECK | crypto.X509StoreFlags.CRL_CHECK_ALL) - # - # [store.add_crl(crl) for crl in crls] - # - # ctx = crypto.X509StoreContext(store, cert) - # try: - # ctx.verify_certificate() # raises X509StoreContextError if unable to verify - # except crypto.X509StoreContextError as e: - # mesg = _unpackContextError(e) - # raise s_exc.BadCertVerify(mesg=mesg) - # return cert - # - # def _getCaCrls(self): - # - # crls = [] - # for cdir in self.certdirs: - # - # crlpath = os.path.join(cdir, 'crls') - # if not os.path.isdir(crlpath): - # continue - # - # for name in os.listdir(crlpath): - # - # if not name.endswith('.crl'): # pragma: no cover - # continue - # - # fullpath = os.path.join(crlpath, name) - # with io.open(fullpath, 'rb') as fd: - # crls.append(crypto.load_crl(crypto.FILETYPE_PEM, fd.read())) - # - # return crls - # - def genClientCert(self, name: AnyStr, outp: OutPutOrNoneType =None) -> None: + def genCodeCert(self, name: str, signas: StrOrNoneType =None, outp: OutPutOrNoneType =None, save: bool=True)\ + -> PkeyAndCertType: + ''' + Generates a code signing keypair. + + Args: + name (str): The name of the code signing cert. + signas (str): The CA keypair to sign the new code keypair with. + outp (synapse.lib.output.Output): The output buffer. + + Examples: + + Generate a code signing cert for the name "The Vertex Project": + + myuserkey, myusercert = cdir.genCodeCert('The Vertex Project') + + Returns: + ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. + ''' + prvkey = self._genPrivKey() + pubkey = prvkey.public_key() + + builder = self._genCertBuilder(name, pubkey) + + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) + builder = builder.add_extension( + c_x509.KeyUsage(digital_signature=True, key_encipherment=False, data_encipherment=False, + key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, + decipher_only=False, content_commitment=False), + critical=False, + ) + builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]), + critical=False) + # XXX FIXME Client encoding value for + # crypto.X509Extension(b'nsCertType', False, b'objsign'), + # builder = builder.add_extension(c_x509.UnrecognizedExtension( + # oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@'), + # critical=False, + # ) + + if signas is not None: + cert = self.signCertAs(builder, signas) + else: + cert = self.selfSignCert(builder, prvkey) + + if save: + keypath = self._savePkeyTo(prvkey, 'code', '%s.key' % name) + if outp is not None: + outp.printf('key saved: %s' % (keypath,)) + + crtpath = self._saveCertTo(cert, 'code', '%s.crt' % name) + if outp is not None: + outp.printf('cert saved: %s' % (crtpath,)) + + return prvkey, cert + + def getCodeKeyPath(self, name: str) -> StrOrNoneType: + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'code', f'{name}.key') + if os.path.isfile(path): + return path + + def getCodeCertPath(self, name: str) -> StrOrNoneType: + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'code', f'{name}.crt') + if os.path.isfile(path): + return path + def getCodeKey(self, name: str) -> s_rsa.PriKey: + + path = self.getCodeKeyPath(name) + if path is None: + return None + + pkey = self._loadKeyPath(path) + return s_rsa.PriKey(pkey) + + def getCodeCert(self, name: str) -> CertOrNoneType: + + path = self.getCodeCertPath(name) + if path is None: # pragma: no cover + return None + + return self._loadCertPath(path) + + def valCodeCert(self, byts: bytes) -> c_x509.Certificate: + ''' + Verify a code cert is valid according to certdir's available CAs and CRLs. + + Args: + byts (bytes): The certificate bytes. + + Returns: + OpenSSL.crypto.X509: The certificate. + ''' + reqext = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]) + + cert = self.loadCertByts(byts) + eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) + # XXX FIXME if eku is none??/ + if reqext != eku.value: + mesg = 'Certificate is not for code signing.' + raise s_exc.BadCertBytes(mesg=mesg) + + crls = self._getCaCrls() + cacerts = self.getCaCerts() + + store = crypto.X509Store() + [store.add_cert(crypto.X509.from_cryptography(cacert)) for cacert in cacerts] + + if crls: + store.set_flags(crypto.X509StoreFlags.CRL_CHECK | crypto.X509StoreFlags.CRL_CHECK_ALL) + [store.add_crl(crypto.CRL.from_cryptography(crl)) for crl in crls] + + ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert)) + try: + ctx.verify_certificate() # raises X509StoreContextError if unable to verify + except crypto.X509StoreContextError as e: + mesg = _unpackContextError(e) + raise s_exc.BadCertVerify(mesg=mesg) + return cert + + def _getCaCrls(self): + + crls = [] + for cdir in self.certdirs: + + crlpath = os.path.join(cdir, 'crls') + if not os.path.isdir(crlpath): + continue + + for name in os.listdir(crlpath): + + if not name.endswith('.crl'): # pragma: no cover + continue + + fullpath = os.path.join(crlpath, name) + with io.open(fullpath, 'rb') as fd: + crl = c_x509.load_pem_x509_crl(fd.read()) + crls.append(crl) + + return crls + + def genClientCert(self, name: str, outp: OutPutOrNoneType =None) -> None: ''' Generates a user PKCS #12 archive. Please note that the resulting file will contain private key material. @@ -2009,7 +2020,7 @@ def genClientCert(self, name: AnyStr, outp: OutPutOrNoneType =None) -> None: if outp is not None: outp.printf('client cert saved: %s' % (crtpath,)) - def valUserCert(self, byts: ByteString, cacerts: Union[List[c_x509.Certificate] | None] =None): + def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | None] =None): ''' Validate the PEM encoded x509 user certificate bytes and return it. @@ -2023,6 +2034,15 @@ def valUserCert(self, byts: ByteString, cacerts: Union[List[c_x509.Certificate] Returns: OpenSSL.crypto.X509: The certificate, if it is valid. ''' + reqext = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]) + + cert = self.loadCertByts(byts) + eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) + + # XXX FIXME if eku is none??/ + if reqext != eku.value: + mesg = 'Certificate is not for client auth.' + raise s_exc.BadCertBytes(mesg=mesg) cert = self.loadCertByts(byts) if cacerts is None: @@ -2038,7 +2058,7 @@ def valUserCert(self, byts: ByteString, cacerts: Union[List[c_x509.Certificate] raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) return cert - def genUserCsr(self, name: AnyStr, outp: OutPutOrNoneType =None) -> ByteString: + def genUserCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: ''' Generates a user certificate signing request. @@ -2074,7 +2094,7 @@ def getCaCert(self, name): ''' return self._loadCertPath(self.getCaCertPath(name)) - def getCaCertBytes(self, name: AnyStr) -> ByteString: + def getCaCertBytes(self, name: str) -> bytes: path = self.getCaCertPath(name) if os.path.exists(path): with open(path, 'rb') as fd: @@ -2105,7 +2125,7 @@ def getCaCerts(self) -> List[c_x509.Certificate]: return retn - def getCaCertPath(self, name: AnyStr) -> StrOrNoneType: + def getCaCertPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a CA certificate. @@ -2143,7 +2163,7 @@ def getCaKey(self, name) -> PkeyOrNoneType: ''' return self._loadKeyPath(self.getCaKeyPath(name)) - def getCaKeyPath(self, name: AnyStr) -> StrOrNoneType: + def getCaKeyPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a CA key. @@ -2163,7 +2183,7 @@ def getCaKeyPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - def getClientCert(self, name: AnyStr) -> Pkcs12OrNoneType: + def getClientCert(self, name: str) -> Pkcs12OrNoneType: ''' Loads the PKCS12 archive object for a given user keypair. @@ -2183,7 +2203,7 @@ def getClientCert(self, name: AnyStr) -> Pkcs12OrNoneType: ''' return self._loadP12Path(self.getClientCertPath(name)) - def getClientCertPath(self, name: AnyStr) -> StrOrNoneType: + def getClientCertPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a client certificate. @@ -2203,7 +2223,7 @@ def getClientCertPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - def getHostCaPath(self, name: AnyStr) -> StrOrNoneType: + def getHostCaPath(self, name: str) -> StrOrNoneType: ''' Gets the path to the CA certificate that issued a given host keypair. @@ -2224,7 +2244,7 @@ def getHostCaPath(self, name: AnyStr) -> StrOrNoneType: return self._getCaPath(cert) - def getHostCert(self, name: AnyStr) -> CertOrNoneType: + def getHostCert(self, name: str) -> CertOrNoneType: ''' Loads the X509 object for a given host keypair. @@ -2247,7 +2267,7 @@ def getHostCert(self, name: AnyStr) -> CertOrNoneType: # return None # return cert.digest('SHA256').decode().lower().replace(':', '') # - def getHostCertPath(self, name: AnyStr) -> StrOrNoneType: + def getHostCertPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a host certificate. @@ -2267,7 +2287,7 @@ def getHostCertPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - def getHostKey(self, name: AnyStr) -> PkeyOrNoneType: + def getHostKey(self, name: str) -> PkeyOrNoneType: ''' Loads the PKey object for a given host keypair. @@ -2284,7 +2304,7 @@ def getHostKey(self, name: AnyStr) -> PkeyOrNoneType: ''' return self._loadKeyPath(self.getHostKeyPath(name)) - def getHostKeyPath(self, name: AnyStr) -> StrOrNoneType: + def getHostKeyPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a host key. @@ -2304,7 +2324,7 @@ def getHostKeyPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - def getUserCaPath(self, name: AnyStr) -> StrOrNoneType: + def getUserCaPath(self, name: str) -> StrOrNoneType: ''' Gets the path to the CA certificate that issued a given user keypair. @@ -2325,7 +2345,7 @@ def getUserCaPath(self, name: AnyStr) -> StrOrNoneType: return self._getCaPath(cert) - def getUserCert(self, name: AnyStr) -> CertOrNoneType: + def getUserCert(self, name: str) -> CertOrNoneType: ''' Loads the X509 object for a given user keypair. @@ -2342,7 +2362,7 @@ def getUserCert(self, name: AnyStr) -> CertOrNoneType: ''' return self._loadCertPath(self.getUserCertPath(name)) - def getUserCertPath(self, name: AnyStr) -> StrOrNoneType: + def getUserCertPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a user certificate. @@ -2362,7 +2382,7 @@ def getUserCertPath(self, name: AnyStr) -> StrOrNoneType: if os.path.isfile(path): return path - def getUserForHost(self, user: AnyStr, host: AnyStr) -> StrOrNoneType: + def getUserForHost(self, user: str, host: str) -> StrOrNoneType: ''' Gets the name of the first existing user cert for a given user and host. @@ -2383,7 +2403,7 @@ def getUserForHost(self, user: AnyStr, host: AnyStr) -> StrOrNoneType: if self.isUserCert(usercert): return usercert - def getUserKey(self, name: AnyStr) -> CertOrNoneType: + def getUserKey(self, name: str) -> CertOrNoneType: ''' Loads the PKey object for a given user keypair. @@ -2401,7 +2421,7 @@ def getUserKey(self, name: AnyStr) -> CertOrNoneType: ''' return self._loadKeyPath(self.getUserKeyPath(name)) - def getUserKeyPath(self, name: AnyStr) -> StrOrNoneType: + def getUserKeyPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a user key. @@ -2432,7 +2452,7 @@ def getUserKeyPath(self, name: AnyStr) -> StrOrNoneType: # path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) # if os.path.isfile(path): # return path - def importFile(self, path: AnyStr, mode: AnyStr, outp: OutPutOrNoneType =None) -> None: + def importFile(self, path: str, mode: str, outp: OutPutOrNoneType =None) -> None: ''' Imports certs and keys into the Synapse cert directory @@ -2472,7 +2492,7 @@ def importFile(self, path: AnyStr, mode: AnyStr, outp: OutPutOrNoneType =None) - if outp is not None: outp.printf('copied %s to %s' % (path, newpath)) - def isCaCert(self, name: AnyStr) -> bool: + def isCaCert(self, name: str) -> bool: ''' Checks if a CA certificate exists. @@ -2489,7 +2509,7 @@ def isCaCert(self, name: AnyStr) -> bool: ''' return self.getCaCertPath(name) is not None - def isClientCert(self, name: AnyStr) -> bool: + def isClientCert(self, name: str) -> bool: ''' Checks if a user client certificate (PKCS12) exists. @@ -2507,7 +2527,7 @@ def isClientCert(self, name: AnyStr) -> bool: crtpath = self._getPathJoin('users', '%s.p12' % name) return os.path.isfile(crtpath) - def isHostCert(self, name: AnyStr) -> bool: + def isHostCert(self, name: str) -> bool: ''' Checks if a host certificate exists. @@ -2524,7 +2544,7 @@ def isHostCert(self, name: AnyStr) -> bool: ''' return self.getHostCertPath(name) is not None - def isUserCert(self, name: AnyStr) -> bool: + def isUserCert(self, name: str) -> bool: ''' Checks if a user certificate exists. @@ -2541,7 +2561,7 @@ def isUserCert(self, name: AnyStr) -> bool: ''' return self.getUserCertPath(name) is not None - def signCertAs(self, builder: c_x509.CertificateBuilder, signas: AnyStr) -> c_x509.Certificate: + def signCertAs(self, builder: c_x509.CertificateBuilder, signas: str) -> c_x509.Certificate: ''' Signs a certificate with a CA keypair. @@ -2576,7 +2596,7 @@ def signCertAs(self, builder: c_x509.CertificateBuilder, signas: AnyStr) -> c_x5 return certificate def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, - signas: AnyStr, + signas: str, outp: OutPutOrNoneType=None, sans=None, save: bool =True) -> PkeyOrNoneAndCertType: @@ -2629,7 +2649,7 @@ def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_ return certificate def signUserCsr(self, xcsr: c_x509.CertificateSigningRequest, - signas: AnyStr, + signas: str, outp: OutPutOrNoneType=None, save: bool =True) -> PkeyOrNoneAndCertType: ''' @@ -2734,30 +2754,30 @@ def getClientSSLContext(self, certname: StrOrNoneType =None) -> ssl.SSLContext: # # return self._getServerSSLContext(hostname=hostname, caname=caname) # - # def getCrlPath(self, name): - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'crls', '%s.crl' % name) - # if os.path.isfile(path): - # return path - # - # def genCrlPath(self, name): - # path = self.getCrlPath(name) - # if path is None: - # s_common.gendir(self.certdirs[0], 'crls') - # path = os.path.join(self.certdirs[0], 'crls', f'{name}.crl') - # return path - # - # def genCaCrl(self, name): - # ''' - # Get the CRL for a given CA. - # - # Args: - # name (str): The CA name. - # - # Returns: - # CRL: The CRL object. - # ''' - # return CRL(self, name) + def getCrlPath(self, name: str) -> StrOrNoneType: + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'crls', '%s.crl' % name) + if os.path.isfile(path): + return path + + def genCrlPath(self, name: str) -> str: + path = self.getCrlPath(name) + if path is None: + s_common.gendir(self.certdirs[0], 'crls') + path = os.path.join(self.certdirs[0], 'crls', f'{name}.crl') + return path + + def genCaCrl(self, name: str) -> CRLNew: + ''' + Get the CRL for a given CA. + + Args: + name (str): The CA name. + + Returns: + CRL: The CRL object. + ''' + return CRLNew(self, name) # # def _getServerSSLContext(self, hostname=None, caname=None): # sslctx = getServerSSLContext() @@ -2822,7 +2842,7 @@ def _checkDupFile(self, path) -> None: def _genPrivKey(self) -> c_rsa.RSAPrivateKey: return c_rsa.generate_private_key(65537, self.crypto_numbits) - def _genCertBuilder(self, name: AnyStr, pubkey: c_types.PublicKeyTypes) -> c_x509.CertificateBuilder: + def _genCertBuilder(self, name: str, pubkey: c_types.PublicKeyTypes) -> c_x509.CertificateBuilder: builder = c_x509.CertificateBuilder() builder = builder.subject_name(c_x509.Name([ c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), @@ -2835,7 +2855,7 @@ def _genCertBuilder(self, name: AnyStr, pubkey: c_types.PublicKeyTypes) -> c_x50 builder = builder.public_key(pubkey) return builder - def _genPkeyCsr(self, name: AnyStr, mode: AnyStr, outp: OutPutOrNoneType=None) -> ByteString: + def _genPkeyCsr(self, name: str, mode: str, outp: OutPutOrNoneType=None) -> bytes: pkey = self._genPrivKey() @@ -2865,21 +2885,21 @@ def _getCaPath(self, cert: c_x509.Certificate) -> StrOrNoneType: issuer = cert.issuer.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] return self.getCaCertPath(issuer.value) - def _getPathBytes(self, path: AnyStr) -> BytesOrNoneType: + def _getPathBytes(self, path: str) -> BytesOrNoneType: if path is None: return None return s_common.getbytes(path) # - def _getPathJoin(self, *paths: AnyStr) -> AnyStr: + def _getPathJoin(self, *paths: str) -> str: '''Get the base certidr path + paths''' return s_common.genpath(self.certdirs[0], *paths) - def _loadCertPath(self, path: AnyStr) -> CertOrNoneType: + def _loadCertPath(self, path: str) -> CertOrNoneType: byts = self._getPathBytes(path) if byts: return self._loadCertByts(byts) - def loadCertByts(self, byts: ByteString) -> c_x509.Certificate: + def loadCertByts(self, byts: bytes) -> c_x509.Certificate: ''' Load a X509 certificate from its PEM encoded bytes. @@ -2910,15 +2930,15 @@ def _loadCertByts(self, byts: bytes) -> c_x509.Certificate: # estr += ' '.join((arg for arg in argv[0] if arg)) # raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') - def _loadCsrPath(self, path: AnyStr) -> Union[c_x509.CertificateSigningRequest | None]: + def _loadCsrPath(self, path: str) -> Union[c_x509.CertificateSigningRequest | None]: byts = self._getPathBytes(path) if byts: return self._loadCsrByts(byts) - def _loadCsrByts(self, byts) -> ByteString: + def _loadCsrByts(self, byts) -> bytes: return c_x509.load_pem_x509_csr(byts) - def _loadKeyPath(self, path: AnyStr) -> PkeyOrNoneType: + def _loadKeyPath(self, path: str) -> PkeyOrNoneType: byts = self._getPathBytes(path) if byts: pkey = c_serialization.load_pem_private_key(byts, password=None) @@ -2929,13 +2949,13 @@ def _loadKeyPath(self, path: AnyStr) -> PkeyOrNoneType: raise s_exc.BadCertBytes(mesg=f'Key at {path} is {type(pkey)}, expected a DSA or RSA key.', path=path) - def _loadP12Path(self, path: AnyStr) -> Pkcs12OrNoneType: + def _loadP12Path(self, path: str) -> Pkcs12OrNoneType: byts = self._getPathBytes(path) if byts: p12 = c_pkcs12.load_pkcs12(byts, password=None) return p12 - def _saveCertTo(self, cert: c_x509.Certificate, *paths: AnyStr): + def _saveCertTo(self, cert: c_x509.Certificate, *paths: str): path = self._getPathJoin(*paths) self._checkDupFile(path) @@ -2948,13 +2968,13 @@ def _saveCertTo(self, cert: c_x509.Certificate, *paths: AnyStr): def _certToByts(self, cert: c_x509.Certificate): return cert.public_bytes(encoding=c_serialization.Encoding.PEM) - def _pkeyToByts(self, pkey: PkeyType) -> ByteString: + def _pkeyToByts(self, pkey: PkeyType) -> bytes: return pkey.private_bytes(encoding=c_serialization.Encoding.PEM, format=c_serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=c_serialization.NoEncryption(), ) - def _savePkeyTo(self, pkey: PkeyType, *paths: AnyStr): + def _savePkeyTo(self, pkey: PkeyType, *paths: str): path = self._getPathJoin(*paths) self._checkDupFile(path) diff --git a/synapse/lib/crypto/rsa.py b/synapse/lib/crypto/rsa.py index 1d82f1a390..7a1395d878 100644 --- a/synapse/lib/crypto/rsa.py +++ b/synapse/lib/crypto/rsa.py @@ -22,7 +22,7 @@ def __init__(self, priv): self.priv = priv # type: c_rsa.RSAPrivateKey self.publ = self.public() - def iden(self): + def iden(self) -> str: ''' Return a SHA256 hash for the public key (to be used as a GUID). @@ -31,7 +31,7 @@ def iden(self): ''' return self.publ.iden() - def sign(self, byts): + def sign(self, byts: bytes) -> bytes: ''' Compute the RSA signature for the given bytestream. @@ -45,7 +45,7 @@ def sign(self, byts): pad = c_padding.PSS(c_padding.MGF1(sha256), c_padding.PSS.MAX_LENGTH) return self.priv.sign(byts, pad, sha256) - def signitem(self, item): + def signitem(self, item) -> bytes: ''' Compute the RSA signature for the given python primitive. diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 87729f1d56..9ae93fedf9 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -595,7 +595,49 @@ def test_certdir_sslctx(self): with self.raises(s_exc.NoCertKey): cdir.getClientSSLContext(certname='newp') + async def test_certdir_codesign(self): + + with self.getCertDir() as cdir: # type: s_certdir.CertDirNew + caname = 'The Vertex Project ROOT CA' + immname = 'The Vertex Project Intermediate CA 00' + codename = 'Vertex Build Pipeline' + + cdir.genCaCert(caname) + cdir.genCaCert(immname, signas=caname) + cdir.genUserCert('notCodeCert', signas=caname, ) + + cdir.genCaCrl(caname)._save() + cdir.genCaCrl(immname)._save() + + cdir.genCodeCert(codename, signas=immname) + rsak = cdir.getCodeKey(codename) + cert = cdir.getCodeCert(codename) + + rsap = rsak.public() + + self.eq(rsak.iden(), rsap.iden()) + + sign = rsak.signitem({'foo': 'bar', 'baz': 'faz'}) + self.true(rsap.verifyitem({'baz': 'faz', 'foo': 'bar'}, sign)) + self.false(rsap.verifyitem({'baz': 'faz', 'foo': 'gronk'}, sign)) + + fp = cdir.getCodeCertPath(codename) + with s_common.genfile(fp) as fd: + byts = fd.read() + vcrt = cdir.valCodeCert(byts) + self.isinstance(vcrt, c_x509.Certificate) + + fp = cdir.getUserCertPath('notCodeCert') + with s_common.genfile(fp) as fd: + bad_byts = fd.read() + with self.raises(s_exc.BadCertBytes): + cdir.valCodeCert(bad_byts) + crl = cdir.genCaCrl(immname) + crl.revoke(vcrt) + with self.raises(s_exc.BadCertVerify) as cm: + cdir.valCodeCert(byts) + self.isin('certificate revoked', cm.exception.get('mesg')) class CertDirTest(s_t_utils.SynTest): From 134eded6b845d7f28c792062d6088d337a583c28 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 13:55:19 +0000 Subject: [PATCH 08/31] Start cutting over to using new certdir implementation, add some server ssl context tests --- synapse/lib/certdir.py | 155 +++++++++++++++--------------- synapse/tests/test_lib_certdir.py | 30 ++++-- 2 files changed, 103 insertions(+), 82 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 94fe9321ce..324096faf8 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -251,7 +251,7 @@ def getServerSSLContext() -> ssl.SSLContext: sslctx.options |= getattr(ssl, "OP_NO_RENEGOTIATION", 0) return sslctx -class CertDir: +class CertDirOld: ''' Certificate loading/generation/signing utilities. @@ -1561,23 +1561,23 @@ def _saveP12To(self, cert, *paths): return path -certdir = CertDir() -def getCertDir() -> CertDir: +certdirold = CertDirOld() +def getCertDir() -> CertDirOld: ''' Get the singleton CertDir instance. Returns: CertDir: A certdir object. ''' - return certdir + return certdirold -def addCertPath(path): - return certdir.addCertPath(path) +def addCertPathOld(path): + return certdirold.addCertPath(path) -def delCertPath(path): - return certdir.delCertPath(path) +def delCertPathOld(path): + return certdirold.delCertPath(path) -def getCertDirn() -> str: +def getCertDirnOld() -> str: ''' Get the expanded default path used by the singleton CertDir instance. @@ -1586,7 +1586,7 @@ def getCertDirn() -> str: ''' return s_common.genpath(defdir) -class CertDirNew: +class CertDir: ''' Certificate loading/generation/signing utilities. @@ -2722,38 +2722,38 @@ def getClientSSLContext(self, certname: StrOrNoneType =None) -> ssl.SSLContext: return sslctx - # def getServerSSLContext(self, hostname=None, caname=None): - # ''' - # Returns an ssl.SSLContext appropriate to listen on a socket - # - # Args: - # - # hostname: If None, the value from socket.gethostname is used to find the key in the servers directory. - # This name should match the not-suffixed part of two files ending in .key and .crt in the hosts - # subdirectory. - # - # caname: If not None, the given name is used to locate a CA certificate used to validate client SSL certs. - # - # Returns: - # ssl.SSLContext: A SSLContext object. - # ''' - # if hostname is not None and hostname.find(',') != -1: - # # multi-hostname SNI routing has been requested - # ctxs = {} - # names = hostname.split(',') - # for name in names: - # ctxs[name] = self._getServerSSLContext(name, caname=caname) - # - # def snifunc(sslsock, sslname, origctx): - # sslsock.context = ctxs.get(sslname, origctx) - # return None - # - # sslctx = ctxs.get(names[0]) - # sslctx.sni_callback = snifunc - # return sslctx - # - # return self._getServerSSLContext(hostname=hostname, caname=caname) - # + def getServerSSLContext(self, hostname: StrOrNoneType =None, caname: StrOrNoneType =None) -> ssl.SSLContext: + ''' + Returns an ssl.SSLContext appropriate to listen on a socket + + Args: + + hostname: If None, the value from socket.gethostname is used to find the key in the servers directory. + This name should match the not-suffixed part of two files ending in .key and .crt in the hosts + subdirectory. + + caname: If not None, the given name is used to locate a CA certificate used to validate client SSL certs. + + Returns: + ssl.SSLContext: A SSLContext object. + ''' + if hostname is not None and hostname.find(',') != -1: + # multi-hostname SNI routing has been requested + ctxs = {} + names = hostname.split(',') + for name in names: + ctxs[name] = self._getServerSSLContext(name, caname=caname) + + def snifunc(sslsock, sslname, origctx): + sslsock.context = ctxs.get(sslname, origctx) + return None + + sslctx = ctxs.get(names[0]) + sslctx.sni_callback = snifunc + return sslctx + + return self._getServerSSLContext(hostname=hostname, caname=caname) + def getCrlPath(self, name: str) -> StrOrNoneType: for cdir in self.certdirs: path = s_common.genpath(cdir, 'crls', '%s.crl' % name) @@ -2778,32 +2778,35 @@ def genCaCrl(self, name: str) -> CRLNew: CRL: The CRL object. ''' return CRLNew(self, name) - # - # def _getServerSSLContext(self, hostname=None, caname=None): - # sslctx = getServerSSLContext() - # - # if hostname is None: - # hostname = socket.gethostname() - # - # certfile = self.getHostCertPath(hostname) - # if certfile is None: - # mesg = f'Missing TLS certificate file for host: {hostname}' - # raise s_exc.NoCertKey(mesg=mesg) - # - # keyfile = self.getHostKeyPath(hostname) - # if keyfile is None: - # mesg = f'Missing TLS key file for host: {hostname}' - # raise s_exc.NoCertKey(mesg=mesg) - # - # sslctx.load_cert_chain(certfile, keyfile) - # - # if caname is not None: - # cafile = self.getCaCertPath(caname) - # sslctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED - # sslctx.load_verify_locations(cafile=cafile) - # - # return sslctx - # + + def _getServerSSLContext(self, hostname=None, caname=None) -> ssl.SSLContext: + sslctx = getServerSSLContext() + + if hostname is None: + hostname = socket.gethostname() + + certfile = self.getHostCertPath(hostname) + if certfile is None: + mesg = f'Missing TLS certificate file for host: {hostname}' + raise s_exc.NoCertKey(mesg=mesg) + + keyfile = self.getHostKeyPath(hostname) + if keyfile is None: + mesg = f'Missing TLS key file for host: {hostname}' + raise s_exc.NoCertKey(mesg=mesg) + + sslctx.load_cert_chain(certfile, keyfile) + + if caname is not None: + cafile = self.getCaCertPath(caname) + if cafile is None: + mesg = f'Missing CA Certificate for {caname}' + raise s_exc.NoSuchCert(mesg=mesg) + sslctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED + sslctx.load_verify_locations(cafile=cafile) + + return sslctx + # def saveCertPem(self, cert, path): # ''' # Save a certificate in PEM format to a file outside the certdir. @@ -2994,23 +2997,23 @@ def _saveP12To(self, byts, *paths): return path -certdirnew = CertDirNew() -def getCertDirnew() -> CertDirNew: +certdir = CertDir() +def getCertDir() -> CertDir: ''' Get the singleton CertDir instance. Returns: CertDir: A certdir object. ''' - return certdirnew + return certdir -def addCertPathNew(path): - return certdirnew.addCertPath(path) +def addCertPath(path): + return certdir.addCertPath(path) -def delCertPathNew(path): - return certdirnew.delCertPath(path) +def delCertPath(path): + return certdir.delCertPath(path) -def getCertDirnnew() -> str: +def getCertDirn() -> str: ''' Get the expanded default path used by the singleton CertDir instance. diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 9ae93fedf9..67b0f17981 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -39,10 +39,10 @@ def getCertDir(self): ''' # create a temp folder and make it a cert dir with self.getTestDir() as dirname: - yield s_certdir.CertDirNew(path=dirname) + yield s_certdir.CertDir(path=dirname) def basic_assertions(self, - cdir: s_certdir.CertDirNew, + cdir: s_certdir.CertDir, cert: c_x509.Certificate, key: s_certdir.PkeyType, cacert: c_x509.Certificate =None): @@ -145,7 +145,7 @@ def basic_assertions(self, self.none(ctx.verify_certificate()) # valid def host_assertions(self, - cdir: s_certdir.CertDirNew, + cdir: s_certdir.CertDir, cert: c_x509.Certificate, key: s_certdir.PkeyType, cacert: c_x509.Certificate = None): @@ -184,7 +184,7 @@ def host_assertions(self, # # self.isin(b'subjectAltName', exts) def user_assertions(self, - cdir: s_certdir.CertDirNew, + cdir: s_certdir.CertDir, cert: c_x509.Certificate, key: s_certdir.PkeyType, cacert: c_x509.Certificate = None): @@ -213,7 +213,7 @@ def user_assertions(self, # self.notin(b'subjectAltName', exts) def p12_assertions(self, - cdir: s_certdir.CertDirNew, + cdir: s_certdir.CertDir, cert: c_x509.Certificate, key: s_certdir.PkeyType, p12: c_pkcs12.PKCS12KeyAndCertificates, @@ -595,9 +595,27 @@ def test_certdir_sslctx(self): with self.raises(s_exc.NoCertKey): cdir.getClientSSLContext(certname='newp') + + caname = 'syntest' + hostname = 'visi.vertex.link' + cdir.genCaCert(caname) + cdir.genHostCert(hostname, signas=caname) + + ctx = cdir.getServerSSLContext(hostname, caname) + self.eq(ctx.verify_mode, ssl.VerifyMode.CERT_REQUIRED) + + ctx = cdir.getServerSSLContext(hostname) + self.eq(ctx.verify_mode, ssl.VerifyMode.CERT_NONE) + + with self.raises(s_exc.NoCertKey): + cdir.getServerSSLContext('haha.newp.com') + + with self.raises(s_exc.NoSuchCert): + cdir.getServerSSLContext(hostname, 'newpca') + async def test_certdir_codesign(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDirNew + with self.getCertDir() as cdir: # type: s_certdir.CertDir caname = 'The Vertex Project ROOT CA' immname = 'The Vertex Project Intermediate CA 00' codename = 'Vertex Build Pipeline' From 4cca69dd6bbea304a2b118d576c393a1267496d3 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 14:16:10 +0000 Subject: [PATCH 09/31] Add getHostCertHash --- synapse/lib/certdir.py | 14 +++++++------- synapse/tests/test_lib_certdir.py | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 324096faf8..2a5ea9bd7e 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -2260,13 +2260,13 @@ def getHostCert(self, name: str) -> CertOrNoneType: OpenSSL.crypto.X509: The certificate, if exists. ''' return self._loadCertPath(self.getHostCertPath(name)) - # - # def getHostCertHash(self, name): - # cert = self.getHostCert(name) - # if cert is None: - # return None - # return cert.digest('SHA256').decode().lower().replace(':', '') - # + + def getHostCertHash(self, name: str) -> StrOrNoneType: + cert = self.getHostCert(name) + if cert is None: + return None + return s_common.ehex(cert.fingerprint(c_hashes.SHA256())) + def getHostCertPath(self, name: str) -> StrOrNoneType: ''' Gets the path to a host certificate. diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 67b0f17981..88fb6cc33d 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -348,6 +348,12 @@ def test_certdir_hosts(self): self.basic_assertions(cdir, cert, key, cacert=cacert) self.host_assertions(cdir, cert, key, cacert=cacert) + # Get cert host hashes + self.none(cdir.getHostCertHash('newp.host')) + chash = cdir.getHostCertHash(hostname) + self.isinstance(chash, str) + self.len(64, chash) + def test_certdir_users(self): with self.getCertDir() as cdir: # type: s_certdir.CertDir caname = 'syntest' From c1ce876b9d30bfff09f0cef1a1a582c81eee9d8b Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 15:04:04 +0000 Subject: [PATCH 10/31] Additional compatibility updates --- synapse/lib/aha.py | 21 ++++--- synapse/lib/certdir.py | 101 ++++++++++++++++-------------- synapse/tests/test_lib_certdir.py | 6 +- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/synapse/lib/aha.py b/synapse/lib/aha.py index 81b7290b7b..d01b326032 100644 --- a/synapse/lib/aha.py +++ b/synapse/lib/aha.py @@ -5,6 +5,8 @@ import logging import collections +import cryptography.x509 as c_x509 + import synapse.exc as s_exc import synapse.common as s_common import synapse.daemon as s_daemon @@ -379,8 +381,9 @@ async def signUserCsr(self, byts): username = f'{ahauser}@{ahanetw}' xcsr = self.aha.certdir._loadCsrByts(byts) - if xcsr.get_subject().CN != username: - mesg = f'Invalid user CSR CN={xcsr.get_subject().CN}.' + name = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value + if name != username: + mesg = f'Invalid user CSR CN={name}.' raise s_exc.BadArg(mesg=mesg) pkey, cert = self.aha.certdir.signUserCsr(xcsr, ahanetw, save=False) @@ -407,8 +410,9 @@ async def signHostCsr(self, byts): hostname = f'{ahaname}.{ahanetw}' xcsr = self.aha.certdir._loadCsrByts(byts) - if xcsr.get_subject().CN != hostname: - mesg = f'Invalid host CSR CN={xcsr.get_subject().CN}.' + name = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value + if name != hostname: + mesg = f'Invalid host CSR CN={name}.' raise s_exc.BadArg(mesg=mesg) pkey, cert = self.aha.certdir.signHostCsr(xcsr, ahanetw, save=False) @@ -422,8 +426,9 @@ async def signUserCsr(self, byts): username = f'{ahauser}@{ahanetw}' xcsr = self.aha.certdir._loadCsrByts(byts) - if xcsr.get_subject().CN != username: - mesg = f'Invalid user CSR CN={xcsr.get_subject().CN}.' + name = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value + if name != username: + mesg = f'Invalid user CSR CN={name}.' raise s_exc.BadArg(mesg=mesg) pkey, cert = self.aha.certdir.signUserCsr(xcsr, ahanetw, save=False) @@ -938,7 +943,7 @@ async def saveUserCert(self, name, userkey, usercert): async def signHostCsr(self, csrtext, signas=None, sans=None): xcsr = self.certdir._loadCsrByts(csrtext.encode()) - hostname = xcsr.get_subject().CN + hostname = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value hostpath = s_common.genpath(self.dirn, 'certs', 'hosts', f'{hostname}.crt') if os.path.isfile(hostpath): @@ -957,7 +962,7 @@ async def signHostCsr(self, csrtext, signas=None, sans=None): async def signUserCsr(self, csrtext, signas=None): xcsr = self.certdir._loadCsrByts(csrtext.encode()) - username = xcsr.get_subject().CN + username = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value userpath = s_common.genpath(self.dirn, 'certs', 'users', f'{username}.crt') if os.path.isfile(userpath): diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 2a5ea9bd7e..b4aa96da65 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -2440,18 +2440,18 @@ def getUserKeyPath(self, name: str) -> StrOrNoneType: path = s_common.genpath(cdir, 'users', '%s.key' % name) if os.path.isfile(path): return path - # - # def getUserCsrPath(self, name): - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'users', '%s.csr' % name) - # if os.path.isfile(path): - # return path - # - # def getHostCsrPath(self, name): - # for cdir in self.certdirs: - # path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) - # if os.path.isfile(path): - # return path + + def getUserCsrPath(self, name: str) -> StrOrNoneType: + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'users', '%s.csr' % name) + if os.path.isfile(path): + return path + + def getHostCsrPath(self, name: str) -> StrOrNoneType: + for cdir in self.certdirs: + path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) + if os.path.isfile(path): + return path def importFile(self, path: str, mode: str, outp: OutPutOrNoneType =None) -> None: ''' Imports certs and keys into the Synapse cert directory @@ -2618,8 +2618,8 @@ def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. ''' pkey = xcsr.public_key() - name = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] - name = name.value + attr = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value return self.genHostCert(name, csr=pkey, signas=signas, outp=outp, sans=sans, save=save) def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_x509.Certificate: @@ -2807,37 +2807,46 @@ def _getServerSSLContext(self, hostname=None, caname=None) -> ssl.SSLContext: return sslctx - # def saveCertPem(self, cert, path): - # ''' - # Save a certificate in PEM format to a file outside the certdir. - # ''' - # with s_common.genfile(path) as fd: - # fd.truncate(0) - # fd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - # - # def savePkeyPem(self, pkey, path): - # ''' - # Save a private key in PEM format to a file outside the certdir. - # ''' - # with s_common.genfile(path) as fd: - # fd.truncate(0) - # fd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - # - # def saveCaCertByts(self, byts): - # cert = self._loadCertByts(byts) - # name = cert.get_subject().CN - # return self._saveCertTo(cert, 'cas', f'{name}.crt') - # - # def saveHostCertByts(self, byts): - # cert = self._loadCertByts(byts) - # name = cert.get_subject().CN - # return self._saveCertTo(cert, 'hosts', f'{name}.crt') - # - # def saveUserCertByts(self, byts): - # cert = self._loadCertByts(byts) - # name = cert.get_subject().CN - # return self._saveCertTo(cert, 'users', f'{name}.crt') - # + # XXX FIXME - Add test_lib_certdir tests for these save APIS + + def saveCertPem(self, cert: c_x509.Certificate, path: str) -> None: + ''' + Save a certificate in PEM format to a file outside the certdir. + ''' + with s_common.genfile(path) as fd: + fd.truncate(0) + fd.write(cert.public_bytes(c_serialization.Encoding.PEM)) + + def savePkeyPem(self, pkey: c_types.PrivateKeyTypes, path: str) -> None: + ''' + Save a private key in PEM format to a file outside the certdir. + ''' + byts = pkey.private_bytes(encoding=c_serialization.Encoding.PEM, + format=c_serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=c_serialization.NoEncryption(), + ) + with s_common.genfile(path) as fd: + fd.truncate(0) + fd.write(byts) + + def saveCaCertByts(self, byts: bytes) -> str: + cert = self._loadCertByts(byts) + attr = cert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value + return self._saveCertTo(cert, 'cas', f'{name}.crt') + + def saveHostCertByts(self, byts: bytes) -> str: + cert = self._loadCertByts(byts) + attr = cert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value + return self._saveCertTo(cert, 'hosts', f'{name}.crt') + + def saveUserCertByts(self, byts: bytes) -> str: + cert = self._loadCertByts(byts) + attr = cert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value + return self._saveCertTo(cert, 'users', f'{name}.crt') + def _checkDupFile(self, path) -> None: if os.path.isfile(path): raise s_exc.DupFileName(mesg=f'Duplicate file {path}', path=path) @@ -2958,7 +2967,7 @@ def _loadP12Path(self, path: str) -> Pkcs12OrNoneType: p12 = c_pkcs12.load_pkcs12(byts, password=None) return p12 - def _saveCertTo(self, cert: c_x509.Certificate, *paths: str): + def _saveCertTo(self, cert: c_x509.Certificate, *paths: str) -> str: path = self._getPathJoin(*paths) self._checkDupFile(path) diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 88fb6cc33d..8d8ab74254 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -478,7 +478,8 @@ def test_certdir_hosts_csr(self): # Generate CA cert and host CSR cdir.genCaCert(caname) cdir.genHostCsr(hostname) - path = cdir._getPathJoin('hosts', hostname + '.csr') + self.none(cdir.getHostCsrPath('newp')) + path = cdir.getHostCsrPath(hostname) xcsr = cdir._loadCsrPath(path) # Sign the CSR as the CA @@ -500,7 +501,8 @@ def test_certdir_users_csr(self): # Generate CA cert and user CSR cdir.genCaCert(caname) cdir.genUserCsr(username) - path = cdir._getPathJoin('users', username + '.csr') + self.none(cdir.getUserCsrPath('newp')) + path = cdir.getUserCsrPath(username) xcsr = cdir._loadCsrPath(path) # Sign the CSR as the CA From 1f1d2595bc40c811df1002f7f8bfed3b8285796e Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 18:29:14 +0000 Subject: [PATCH 11/31] Some more cleanup --- synapse/tests/test_telepath.py | 16 ++++++++-------- synapse/tests/test_tools_docker_validate.py | 4 ++-- synapse/tools/aha/easycert.py | 10 ++++------ synapse/tools/docker/validate.py | 19 ++++++++----------- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/synapse/tests/test_telepath.py b/synapse/tests/test_telepath.py index 11ff9420a9..c8ba92ff7d 100644 --- a/synapse/tests/test_telepath.py +++ b/synapse/tests/test_telepath.py @@ -8,7 +8,7 @@ from unittest import mock -logger = logging.getLogger(__name__) +import cryptography.hazmat.primitives.hashes as c_hashes import synapse.exc as s_exc import synapse.glob as s_glob @@ -21,11 +21,11 @@ import synapse.lib.link as s_link import synapse.lib.share as s_share import synapse.lib.certdir as s_certdir -import synapse.lib.httpapi as s_httpapi import synapse.lib.version as s_version import synapse.tests.utils as s_t_utils -from synapse.tests.utils import alist + +logger = logging.getLogger(__name__) class Boom: pass @@ -248,7 +248,7 @@ async def test_telepath_basics(self): # check an async generator return channel genr = prox.corogenr(3) self.true(isinstance(genr, s_telepath.GenrIter)) - self.eq((0, 1, 2), await alist(genr)) + self.eq((0, 1, 2), await s_t_utils.alist(genr)) # check async generator explodes channel genr = prox.agenrboom() @@ -343,11 +343,11 @@ async def test_telepath_no_sess(self): # check a generator return channel genr = await prox.genr() - self.eq((10, 20, 30), await alist(genr)) + self.eq((10, 20, 30), await s_t_utils.alist(genr)) # check an async generator return channel genr = prox.corogenr(3) - self.eq((0, 1, 2), await alist(genr)) + self.eq((0, 1, 2), await s_t_utils.alist(genr)) await self.asyncraises(s_exc.NoSuchMeth, prox.raze()) @@ -398,7 +398,7 @@ async def test_telepath_tls_certhash(self): hostkey, hostcert = certdir.genHostCert(hostname, signas='loopy') self.none(certdir.getHostCertHash('newp.newp.newp')) - certhash = hostcert.digest('sha256').decode().lower().replace(':', '') + certhash = s_common.ehex(hostcert.fingerprint(c_hashes.SHA256())) host, port = await dmon.listen(f'ssl://{hostname}:0') dmon.share('foo', foo) @@ -662,7 +662,7 @@ async def test_telepath_aware(self): key: 'synapse.tests.test_telepath.CustomShare'}) # and we can still use the first obj we made - ret = await alist(obj.custgenr(3)) + ret = await s_t_utils.alist(obj.custgenr(3)) self.eq(ret, [0, 1, 2]) # check that a dynamic share works diff --git a/synapse/tests/test_tools_docker_validate.py b/synapse/tests/test_tools_docker_validate.py index cbdf11d529..7580b7dff6 100644 --- a/synapse/tests/test_tools_docker_validate.py +++ b/synapse/tests/test_tools_docker_validate.py @@ -2,7 +2,7 @@ import json import unittest.mock as mock -from OpenSSL import crypto +import cryptography.hazmat.primitives.serialization as c_serialization import synapse.lib.certdir as s_certdir import synapse.tests.utils as s_t_utils @@ -42,7 +42,7 @@ def test_tool_docker_validate(self): certdir.genCodeCert('signer', signas='cosignTest') sign_cert = certdir.getCodeCert('signer') - der_byts = crypto.dump_certificate(crypto.FILETYPE_ASN1, sign_cert) + der_byts = sign_cert.public_bytes(c_serialization.Encoding.DER) test_resp = {'Cert': {'Raw': base64.b64encode(der_byts).decode()}} # getCosignSignature diff --git a/synapse/tools/aha/easycert.py b/synapse/tools/aha/easycert.py index 1ea8a128f1..c8a7284ca7 100644 --- a/synapse/tools/aha/easycert.py +++ b/synapse/tools/aha/easycert.py @@ -2,11 +2,9 @@ import asyncio import logging import argparse -import contextlib -from OpenSSL import crypto +import cryptography.x509 as c_x509 -import synapse.exc as s_exc import synapse.common as s_common import synapse.telepath as s_telepath @@ -32,21 +30,21 @@ async def _main(argv, outp): certbyts = await prox.getCaCert(name) if not certbyts: certbyts = await prox.genCaCert(name) - cert = crypto.load_certificate(crypto.FILETYPE_PEM, certbyts) + cert = c_x509.load_pem_x509_certificate(certbyts.encode()) path = cdir._saveCertTo(cert, 'cas', f'{name}.crt') outp.printf(f'Saved CA cert to {path}') return 0 elif opts.server: csr = cdir.genHostCsr(name, outp=outp) certbyts = await prox.signHostCsr(csr.decode(), signas=opts.network, sans=opts.server_sans) - cert = crypto.load_certificate(crypto.FILETYPE_PEM, certbyts) + cert = c_x509.load_pem_x509_certificate(certbyts.encode()) path = cdir._saveCertTo(cert, 'hosts', f'{name}.crt') outp.printf(f'crt saved: {path}') return 0 else: csr = cdir.genUserCsr(name, outp=outp) certbyts = await prox.signUserCsr(csr.decode(), signas=opts.network) - cert = crypto.load_certificate(crypto.FILETYPE_PEM, certbyts) + cert = c_x509.load_pem_x509_certificate(certbyts.encode()) path = cdir._saveCertTo(cert, 'users', f'{name}.crt') outp.printf(f'crt saved: {path}') return 0 diff --git a/synapse/tools/docker/validate.py b/synapse/tools/docker/validate.py index 3795287d18..5a56aa5bff 100644 --- a/synapse/tools/docker/validate.py +++ b/synapse/tools/docker/validate.py @@ -14,7 +14,8 @@ import synapse.lib.output as s_outp import synapse.lib.certdir as s_certdir -from OpenSSL import crypto +import cryptography.x509 as c_x509 +import cryptography.hazmat.primitives.serialization as c_serialization def checkCosign(outp): args = ('cosign', 'version') @@ -65,16 +66,11 @@ def checkCRL(outp, sigd, certdir): byts = base64.b64decode(sigd.get('Cert', {}).get('Raw', '')) try: - cert = crypto.load_certificate(crypto.FILETYPE_ASN1, byts) - pem_byts = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - except crypto.Error as e: # pragma: no cover + cert = c_x509.load_der_x509_certificate(byts) + pem_byts = cert.public_bytes(c_serialization.Encoding.PEM) + except Exception as e: # pragma: no cover # Unwrap pyopenssl's exception_from_error_queue - estr = '' - for argv in e.args: - if estr: - estr += ', ' - estr += ' '.join((arg for arg in argv[0] if arg)) - outp.printf(f'Failed to load signature bytes: {byts}') + outp.printf(f'Failed to load signature bytes: {e} {byts}') return False try: @@ -89,7 +85,8 @@ def checkCRL(outp, sigd, certdir): return False # Return the pubkey bytes in PEM format - return crypto.dump_publickey(crypto.FILETYPE_PEM, cert.get_pubkey()) + return cert.public_key().public_bytes(encoding=c_serialization.Encoding.PEM, + format=c_serialization.PublicFormat.SubjectPublicKeyInfo) def checkCosignSignature(outp, pubk_byts, image_to_verify): with s_common.getTempDir() as dirn: From 2d66bf27597bf500bfc4146aaefaf5073d444bf3 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 18:47:03 +0000 Subject: [PATCH 12/31] Remvoe old certdir interface --- synapse/lib/certdir.py | 1406 +---------------------------- synapse/tests/test_lib_certdir.py | 601 ------------ 2 files changed, 3 insertions(+), 2004 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index b4aa96da65..6a3d4bb956 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -98,72 +98,7 @@ def _unpackContextError(e: crypto.X509StoreContextError) -> str: mesg = 'Certficate failed to verify.' return mesg -class CRL: - - def __init__(self, certdir, name): - - self.name = name - self.certdir = certdir - self.path = certdir.genCrlPath(name) - - if os.path.isfile(self.path): - with io.open(self.path, 'rb') as fd: - self.opensslcrl = crypto.load_crl(crypto.FILETYPE_PEM, fd.read()) - - else: - self.opensslcrl = crypto.CRL() - - def revoke(self, cert): - ''' - Revoke a certificate with the CRL. - - Args: - cert (cryto.X509): The certificate to revoke. - - Returns: - None - ''' - try: - self._verify(cert) - except s_exc.BadCertVerify as e: - raise s_exc.BadCertVerify(mesg=f'Failed to validate that certificate was signed by {self.name}') from e - timestamp = time.strftime('%Y%m%d%H%M%SZ').encode() - revoked = crypto.Revoked() - revoked.set_reason(None) - revoked.set_rev_date(timestamp) - revoked.set_serial(b'%x' % cert.get_serial_number()) - - self.opensslcrl.add_revoked(revoked) - self._save(timestamp) - - def _verify(self, cert): - # Verify the cert was signed by the CA in self.name - cacert = self.certdir.getCaCert(self.name) - store = crypto.X509Store() - store.add_cert(cacert) - store.set_flags(crypto.X509StoreFlags.PARTIAL_CHAIN) - ctx = crypto.X509StoreContext(store, cert,) - try: - ctx.verify_certificate() - except crypto.X509StoreContextError as e: - raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) from None - - def _save(self, timestamp=None): - - if timestamp is None: - timestamp = time.strftime('%Y%m%d%H%M%SZ').encode() - - pkey = self.certdir.getCaKey(self.name) - cert = self.certdir.getCaCert(self.name) - - self.opensslcrl.set_lastUpdate(timestamp) - self.opensslcrl.sign(cert, pkey, b'sha256') - - with s_common.genfile(self.path) as fd: - fd.truncate(0) - fd.write(crypto.dump_crl(crypto.FILETYPE_PEM, self.opensslcrl)) - -class CRLNew: +class Crl: def __init__(self, certdir, name): @@ -251,1341 +186,6 @@ def getServerSSLContext() -> ssl.SSLContext: sslctx.options |= getattr(ssl, "OP_NO_RENEGOTIATION", 0) return sslctx -class CertDirOld: - ''' - Certificate loading/generation/signing utilities. - - Features: - * Locates and load certificates, keys, and certificate signing requests (CSRs). - * Generates keypairs for users, hosts, and certificate authorities (CAs), supports both signed and self-signed. - * Generates certificate signing requests (CSRs) for users, hosts, and certificate authorities (CAs). - * Signs certificate signing requests (CSRs). - * Generates PKCS#12 archives for use in browser. - - Args: - path (str): Optional path which can override the default path directory. - - Notes: - * All certificates will be loaded from and written to ~/.syn/certs by default. Set the environment variable - SYN_CERT_DIR to override. - * All certificate generation methods create 4096 bit RSA keypairs. - * All certificate signing methods use sha256 as the signature algorithm. - * CertDir does not currently support signing CA CSRs. - ''' - - def __init__(self, path=None): - self.crypto_numbits = 4096 - self.signing_digest = 'sha256' - - self.certdirs = [] - self.pathrefs = collections.defaultdict(int) - - if path is None: - path = (defdir,) - - if not isinstance(path, (list, tuple)): - path = (path,) - - for p in path: - self.addCertPath(p) - - def addCertPath(self, *path): - - fullpath = s_common.genpath(*path) - self.pathrefs[fullpath] += 1 - - if self.pathrefs[fullpath] == 1: - self.certdirs.append(fullpath) - - def delCertPath(self, *path): - fullpath = s_common.genpath(*path) - self.pathrefs[fullpath] -= 1 - if self.pathrefs[fullpath] <= 0: - self.certdirs.remove(fullpath) - self.pathrefs.pop(fullpath, None) - - def genCaCert(self, name, signas=None, outp=None, save=True): - ''' - Generates a CA keypair. - - Args: - name (str): The name of the CA keypair. - signas (str): The CA keypair to sign the new CA with. - outp (synapse.lib.output.Output): The output buffer. - - Examples: - Make a CA named "myca": - - mycakey, mycacert = cdir.genCaCert('myca') - - Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the private key and certificate objects. - ''' - pkey, cert = self._genBasePkeyCert(name) - ext0 = crypto.X509Extension(b'basicConstraints', False, b'CA:TRUE') - cert.add_extensions([ext0]) - - if signas is not None: - self.signCertAs(cert, signas) - else: - self.selfSignCert(cert, pkey) - - if save: - - keypath = self._savePkeyTo(pkey, 'cas', '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) - - crtpath = self._saveCertTo(cert, 'cas', '%s.crt' % name) - if outp is not None: - outp.printf('cert saved: %s' % (crtpath,)) - - return pkey, cert - - def genHostCert(self, name, signas=None, outp=None, csr=None, sans=None, save=True): - ''' - Generates a host keypair. - - Args: - name (str): The name of the host keypair. - signas (str): The CA keypair to sign the new host keypair with. - outp (synapse.lib.output.Output): The output buffer. - csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. - sans (list): List of subject alternative names. - - Examples: - Make a host keypair named "myhost": - - myhostkey, myhostcert = cdir.genHostCert('myhost') - - Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the private key and certificate objects. - ''' - pkey, cert = self._genBasePkeyCert(name, pkey=csr) - - ext_sans = {'DNS:' + name} - if isinstance(sans, str): - ext_sans = ext_sans.union(sans.split(',')) - ext_sans = ','.join(sorted(ext_sans)) - - cert.add_extensions([ - crypto.X509Extension(b'nsCertType', False, b'server'), - crypto.X509Extension(b'keyUsage', False, b'digitalSignature,keyEncipherment'), - crypto.X509Extension(b'extendedKeyUsage', False, b'serverAuth'), - crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE'), - crypto.X509Extension(b'subjectAltName', False, ext_sans.encode('utf-8')), - ]) - - if signas is not None: - self.signCertAs(cert, signas) - else: - self.selfSignCert(cert, pkey) - - if save: - if not pkey._only_public: - keypath = self._savePkeyTo(pkey, 'hosts', '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) - - crtpath = self._saveCertTo(cert, 'hosts', '%s.crt' % name) - if outp is not None: - outp.printf('cert saved: %s' % (crtpath,)) - - return pkey, cert - - def genHostCsr(self, name, outp=None): - ''' - Generates a host certificate signing request. - - Args: - name (str): The name of the host CSR. - outp (synapse.lib.output.Output): The output buffer. - - Examples: - Generate a CSR for the host key named "myhost": - - cdir.genHostCsr('myhost') - - Returns: - bytes: The bytes of the CSR. - ''' - return self._genPkeyCsr(name, 'hosts', outp=outp) - - def genUserCert(self, name, signas=None, outp=None, csr=None, save=True): - ''' - Generates a user keypair. - - Args: - name (str): The name of the user keypair. - signas (str): The CA keypair to sign the new user keypair with. - outp (synapse.lib.output.Output): The output buffer. - csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. - - Examples: - Generate a user cert for the user "myuser": - - myuserkey, myusercert = cdir.genUserCert('myuser') - - Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. - ''' - pkey, cert = self._genBasePkeyCert(name, pkey=csr) - - cert.add_extensions([ - crypto.X509Extension(b'nsCertType', False, b'client'), - crypto.X509Extension(b'keyUsage', False, b'digitalSignature'), - crypto.X509Extension(b'extendedKeyUsage', False, b'clientAuth'), - crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE'), - ]) - - if signas is not None: - self.signCertAs(cert, signas) - else: - self.selfSignCert(cert, pkey) - - if save: - crtpath = self._saveCertTo(cert, 'users', '%s.crt' % name) - if outp is not None: - outp.printf('cert saved: %s' % (crtpath,)) - - if not pkey._only_public: - keypath = self._savePkeyTo(pkey, 'users', '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) - - return pkey, cert - - def genCodeCert(self, name, signas=None, outp=None, save=True): - ''' - Generates a code signing keypair. - - Args: - name (str): The name of the code signing cert. - signas (str): The CA keypair to sign the new code keypair with. - outp (synapse.lib.output.Output): The output buffer. - - Examples: - - Generate a code signing cert for the name "The Vertex Project": - - myuserkey, myusercert = cdir.genCodeCert('The Vertex Project') - - Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. - ''' - pkey, cert = self._genBasePkeyCert(name) - - cert.add_extensions([ - crypto.X509Extension(b'nsCertType', False, b'objsign'), - crypto.X509Extension(b'keyUsage', False, b'digitalSignature'), - crypto.X509Extension(b'extendedKeyUsage', False, b'codeSigning'), - crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE'), - ]) - - if signas is not None: - self.signCertAs(cert, signas) - - if save: - crtpath = self._saveCertTo(cert, 'code', '%s.crt' % name) - if outp is not None: - outp.printf('cert saved: %s' % (crtpath,)) - - if not pkey._only_public: - keypath = self._savePkeyTo(pkey, 'code', '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) - - return pkey, cert - - def getCodeKeyPath(self, name): - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'code', f'{name}.key') - if os.path.isfile(path): - return path - - def getCodeCertPath(self, name): - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'code', f'{name}.crt') - if os.path.isfile(path): - return path - - def getCodeKey(self, name): - - path = self.getCodeKeyPath(name) - if path is None: - return None - - pkey = self._loadKeyPath(path) - return s_rsa.PriKey(pkey.to_cryptography_key()) - - def getCodeCert(self, name): - - path = self.getCodeCertPath(name) - if path is None: # pragma: no cover - return None - - return self._loadCertPath(path) - - def _getCertExt(self, cert, name): - for i in range(cert.get_extension_count()): - ext = cert.get_extension(i) - if ext.get_short_name() == name: - return ext.get_data() - - def valCodeCert(self, byts): - ''' - Verify a code cert is valid according to certdir's available CAs and CRLs. - - Args: - byts (bytes): The certificate bytes. - - Returns: - OpenSSL.crypto.X509: The certificate. - ''' - - reqext = crypto.X509Extension(b'extendedKeyUsage', False, b'codeSigning') - - cert = self.loadCertByts(byts) - if self._getCertExt(cert, b'extendedKeyUsage') != reqext.get_data(): - mesg = 'Certificate is not for code signing.' - raise s_exc.BadCertBytes(mesg=mesg) - - crls = self._getCaCrls() - cacerts = self.getCaCerts() - - store = crypto.X509Store() - [store.add_cert(cacert) for cacert in cacerts] - - if crls: - - store.set_flags(crypto.X509StoreFlags.CRL_CHECK | crypto.X509StoreFlags.CRL_CHECK_ALL) - - [store.add_crl(crl) for crl in crls] - - ctx = crypto.X509StoreContext(store, cert) - try: - ctx.verify_certificate() # raises X509StoreContextError if unable to verify - except crypto.X509StoreContextError as e: - mesg = _unpackContextError(e) - raise s_exc.BadCertVerify(mesg=mesg) - return cert - - def _getCaCrls(self): - - crls = [] - for cdir in self.certdirs: - - crlpath = os.path.join(cdir, 'crls') - if not os.path.isdir(crlpath): - continue - - for name in os.listdir(crlpath): - - if not name.endswith('.crl'): # pragma: no cover - continue - - fullpath = os.path.join(crlpath, name) - with io.open(fullpath, 'rb') as fd: - crls.append(crypto.load_crl(crypto.FILETYPE_PEM, fd.read())) - - return crls - - def genClientCert(self, name, outp=None): - ''' - Generates a user PKCS #12 archive. - Please note that the resulting file will contain private key material. - - Args: - name (str): The name of the user keypair. - outp (synapse.lib.output.Output): The output buffer. - - Examples: - Make the PKC12 object for user "myuser": - - myuserpkcs12 = cdir.genClientCert('myuser') - - Returns: - OpenSSL.crypto.PKCS12: The PKCS #12 archive. - ''' - ucert = self.getUserCert(name) - if not ucert: - raise s_exc.NoSuchFile(mesg='missing User cert', name=name) - - capath = self._getCaPath(ucert) - cacert = self._loadCertPath(capath) - if not cacert: - raise s_exc.NoSuchFile(mesg='missing CA cert', path=capath) - - ukey = self.getUserKey(name) - if not ukey: - raise s_exc.NoSuchFile(mesg='missing User private key', name=name) - - ccert = crypto.PKCS12() - ccert.set_friendlyname(name.encode('utf-8')) - ccert.set_ca_certificates([cacert]) - ccert.set_certificate(ucert) - ccert.set_privatekey(ukey) - - crtpath = self._saveP12To(ccert, 'users', '%s.p12' % name) - if outp is not None: - outp.printf('client cert saved: %s' % (crtpath,)) - - def valUserCert(self, byts, cacerts=None): - ''' - Validate the PEM encoded x509 user certificate bytes and return it. - - Args: - byts (bytes): The bytes for the User Certificate. - cacerts (tuple): A tuple of OpenSSL.crypto.X509 CA Certificates. - - Raises: - BadCertVerify: If the certificate is not valid. - - Returns: - OpenSSL.crypto.X509: The certificate, if it is valid. - ''' - cert = self.loadCertByts(byts) - - if cacerts is None: - cacerts = self.getCaCerts() - - store = crypto.X509Store() - [store.add_cert(cacert) for cacert in cacerts] - - ctx = crypto.X509StoreContext(store, cert) - try: - ctx.verify_certificate() - except crypto.X509StoreContextError as e: - raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) - return cert - - def genUserCsr(self, name, outp=None): - ''' - Generates a user certificate signing request. - - Args: - name (str): The name of the user CSR. - outp (synapse.lib.output.Output): The output buffer. - - Examples: - Generate a CSR for the user "myuser": - - cdir.genUserCsr('myuser') - - Returns: - bytes: The bytes of the CSR. - ''' - return self._genPkeyCsr(name, 'users', outp=outp) - - def getCaCert(self, name): - ''' - Loads the X509 object for a given CA. - - Args: - name (str): The name of the CA keypair. - - Examples: - Get the certificate for the CA "myca" - - mycacert = cdir.getCaCert('myca') - - Returns: - OpenSSL.crypto.X509: The certificate, if exists. - ''' - return self._loadCertPath(self.getCaCertPath(name)) - - def getCaCertBytes(self, name): - path = self.getCaCertPath(name) - if os.path.exists(path): - with open(path, 'rb') as fd: - return fd.read() - - def getCaCerts(self): - ''' - Return a list of CA certs from the CertDir. - - Returns: - [OpenSSL.crypto.X509]: List of CA certificates. - ''' - retn = [] - - for cdir in self.certdirs: - - path = s_common.genpath(cdir, 'cas') - if not os.path.isdir(path): - continue - - for name in os.listdir(path): - - if not name.endswith('.crt'): # pragma: no cover - continue - - full = s_common.genpath(cdir, 'cas', name) - retn.append(self._loadCertPath(full)) - - return retn - - def getCaCertPath(self, name): - ''' - Gets the path to a CA certificate. - - Args: - name (str): The name of the CA keypair. - - Examples: - Get the path to the CA certificate for the CA "myca": - - mypath = cdir.getCACertPath('myca') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'cas', '%s.crt' % name) - if os.path.isfile(path): - return path - - def getCaKey(self, name): - ''' - Loads the PKey object for a given CA keypair. - - Args: - name (str): The name of the CA keypair. - - Examples: - Get the private key for the CA "myca": - - mycakey = cdir.getCaKey('myca') - - Returns: - OpenSSL.crypto.PKey: The private key, if exists. - ''' - return self._loadKeyPath(self.getCaKeyPath(name)) - - def getCaKeyPath(self, name): - ''' - Gets the path to a CA key. - - Args: - name (str): The name of the CA keypair. - - Examples: - Get the path to the private key for the CA "myca": - - mypath = cdir.getCAKeyPath('myca') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'cas', '%s.key' % name) - if os.path.isfile(path): - return path - - def getClientCert(self, name): - ''' - Loads the PKCS12 archive object for a given user keypair. - - Args: - name (str): The name of the user keypair. - - Examples: - Get the PKCS12 object for the user "myuser": - - mypkcs12 = cdir.getClientCert('myuser') - - Notes: - The PKCS12 archive will contain private key material if it was created with CertDir or the easycert tool - - Returns: - OpenSSL.crypto.PKCS12: The PKCS12 archive, if exists. - ''' - return self._loadP12Path(self.getClientCertPath(name)) - - def getClientCertPath(self, name): - ''' - Gets the path to a client certificate. - - Args: - name (str): The name of the client keypair. - - Examples: - Get the path to the client certificate for "myuser": - - mypath = cdir.getClientCertPath('myuser') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'users', '%s.p12' % name) - if os.path.isfile(path): - return path - - def getHostCaPath(self, name): - ''' - Gets the path to the CA certificate that issued a given host keypair. - - Args: - name (str): The name of the host keypair. - - Examples: - Get the path to the CA cert which issue the cert for "myhost": - - mypath = cdir.getHostCaPath('myhost') - - Returns: - str: The path if exists. - ''' - cert = self.getHostCert(name) - if cert is None: - return None - - return self._getCaPath(cert) - - def getHostCert(self, name): - ''' - Loads the X509 object for a given host keypair. - - Args: - name (str): The name of the host keypair. - - Examples: - Get the certificate object for the host "myhost": - - myhostcert = cdir.getHostCert('myhost') - - Returns: - OpenSSL.crypto.X509: The certificate, if exists. - ''' - return self._loadCertPath(self.getHostCertPath(name)) - - def getHostCertHash(self, name): - cert = self.getHostCert(name) - if cert is None: - return None - return cert.digest('SHA256').decode().lower().replace(':', '') - - def getHostCertPath(self, name): - ''' - Gets the path to a host certificate. - - Args: - name (str): The name of the host keypair. - - Examples: - Get the path to the host certificate for the host "myhost": - - mypath = cdir.getHostCertPath('myhost') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'hosts', '%s.crt' % name) - if os.path.isfile(path): - return path - - def getHostKey(self, name): - ''' - Loads the PKey object for a given host keypair. - - Args: - name (str): The name of the host keypair. - - Examples: - Get the private key object for the host "myhost": - - myhostkey = cdir.getHostKey('myhost') - - Returns: - OpenSSL.crypto.PKey: The private key, if exists. - ''' - return self._loadKeyPath(self.getHostKeyPath(name)) - - def getHostKeyPath(self, name): - ''' - Gets the path to a host key. - - Args: - name (str): The name of the host keypair. - - Examples: - Get the path to the host key for the host "myhost": - - mypath = cdir.getHostKeyPath('myhost') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'hosts', '%s.key' % name) - if os.path.isfile(path): - return path - - def getUserCaPath(self, name): - ''' - Gets the path to the CA certificate that issued a given user keypair. - - Args: - name (str): The name of the user keypair. - - Examples: - Get the path to the CA cert which issue the cert for "myuser": - - mypath = cdir.getUserCaPath('myuser') - - Returns: - str: The path if exists. - ''' - cert = self.getUserCert(name) - if cert is None: - return None - - return self._getCaPath(cert) - - def getUserCert(self, name): - ''' - Loads the X509 object for a given user keypair. - - Args: - name (str): The name of the user keypair. - - Examples: - Get the certificate object for the user "myuser": - - myusercert = cdir.getUserCert('myuser') - - Returns: - OpenSSL.crypto.X509: The certificate, if exists. - ''' - return self._loadCertPath(self.getUserCertPath(name)) - - def getUserCertPath(self, name): - ''' - Gets the path to a user certificate. - - Args: - name (str): The name of the user keypair. - - Examples: - Get the path for the user cert for "myuser": - - mypath = cdir.getUserCertPath('myuser') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'users', '%s.crt' % name) - if os.path.isfile(path): - return path - - def getUserForHost(self, user, host): - ''' - Gets the name of the first existing user cert for a given user and host. - - Args: - user (str): The name of the user. - host (str): The name of the host. - - Examples: - Get the name for the "myuser" user cert at "cool.vertex.link": - - usercertname = cdir.getUserForHost('myuser', 'cool.vertex.link') - - Returns: - str: The cert name, if exists. - ''' - for name in iterFqdnUp(host): - usercert = '%s@%s' % (user, name) - if self.isUserCert(usercert): - return usercert - - def getUserKey(self, name): - ''' - Loads the PKey object for a given user keypair. - - - Args: - name (str): The name of the user keypair. - - Examples: - Get the key object for the user key for "myuser": - - myuserkey = cdir.getUserKey('myuser') - - Returns: - OpenSSL.crypto.PKey: The private key, if exists. - ''' - return self._loadKeyPath(self.getUserKeyPath(name)) - - def getUserKeyPath(self, name): - ''' - Gets the path to a user key. - - Args: - name (str): The name of the user keypair. - - Examples: - Get the path to the user key for "myuser": - - mypath = cdir.getUserKeyPath('myuser') - - Returns: - str: The path if exists. - ''' - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'users', '%s.key' % name) - if os.path.isfile(path): - return path - - def getUserCsrPath(self, name): - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'users', '%s.csr' % name) - if os.path.isfile(path): - return path - - def getHostCsrPath(self, name): - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) - if os.path.isfile(path): - return path - def importFile(self, path, mode, outp=None): - ''' - Imports certs and keys into the Synapse cert directory - - Args: - path (str): The path of the file to be imported. - mode (str): The certdir subdirectory to import the file into. - - Examples: - Import CA certifciate 'mycoolca.crt' to the 'cas' directory. - - certdir.importFile('mycoolca.crt', 'cas') - - Notes: - importFile does not perform any validation on the files it imports. - - Returns: - None - ''' - if not os.path.isfile(path): - raise s_exc.NoSuchFile(mesg=f'File {path} does not exist', path=path) - - fname = os.path.split(path)[1] - parts = fname.rsplit('.', 1) - ext = parts[1] if len(parts) == 2 else None - - if not ext or ext not in ('crt', 'key', 'p12'): - mesg = 'importFile only supports .crt, .key, .p12 extensions' - raise s_exc.BadFileExt(mesg=mesg, ext=ext) - - newpath = s_common.genpath(self.certdirs[0], mode, fname) - if os.path.isfile(newpath): - raise s_exc.FileExists(mesg=f'File {newpath} already exists', path=path) - - s_common.gendir(os.path.dirname(newpath)) - - shutil.copy(path, newpath) - if outp is not None: - outp.printf('copied %s to %s' % (path, newpath)) - - def isCaCert(self, name): - ''' - Checks if a CA certificate exists. - - Args: - name (str): The name of the CA keypair. - - Examples: - Check if the CA certificate for "myca" exists: - - exists = cdir.isCaCert('myca') - - Returns: - bool: True if the certificate is present, False otherwise. - ''' - return self.getCaCertPath(name) is not None - - def isClientCert(self, name): - ''' - Checks if a user client certificate (PKCS12) exists. - - Args: - name (str): The name of the user keypair. - - Examples: - Check if the client certificate "myuser" exists: - - exists = cdir.isClientCert('myuser') - - Returns: - bool: True if the certificate is present, False otherwise. - ''' - crtpath = self._getPathJoin('users', '%s.p12' % name) - return os.path.isfile(crtpath) - - def isHostCert(self, name): - ''' - Checks if a host certificate exists. - - Args: - name (str): The name of the host keypair. - - Examples: - Check if the host cert "myhost" exists: - - exists = cdir.isUserCert('myhost') - - Returns: - bool: True if the certificate is present, False otherwise. - ''' - return self.getHostCertPath(name) is not None - - def isUserCert(self, name): - ''' - Checks if a user certificate exists. - - Args: - name (str): The name of the user keypair. - - Examples: - Check if the user cert "myuser" exists: - - exists = cdir.isUserCert('myuser') - - Returns: - bool: True if the certificate is present, False otherwise. - ''' - return self.getUserCertPath(name) is not None - - def signCertAs(self, cert, signas): - ''' - Signs a certificate with a CA keypair. - - Args: - cert (OpenSSL.crypto.X509): The certificate to sign. - signas (str): The CA keypair name to sign the new keypair with. - - Examples: - Sign a certificate with the CA "myca": - - cdir.signCertAs(mycert, 'myca') - - Returns: - None - ''' - cakey = self.getCaKey(signas) - if cakey is None: - raise s_exc.NoCertKey(mesg=f'Missing .key for {signas}') - cacert = self.getCaCert(signas) - if cacert is None: - raise s_exc.NoCertKey(mesg=f'Missing .crt for {signas}') - - cert.set_issuer(cacert.get_subject()) - cert.sign(cakey, self.signing_digest) - - def signHostCsr(self, xcsr, signas, outp=None, sans=None, save=True): - ''' - Signs a host CSR with a CA keypair. - - Args: - xcsr (OpenSSL.crypto.X509Req): The certificate signing request. - signas (str): The CA keypair name to sign the CSR with. - outp (synapse.lib.output.Output): The output buffer. - sans (list): List of subject alternative names. - - Examples: - Sign a host key with the CA "myca": - - cdir.signHostCsr(mycsr, 'myca') - - Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. - ''' - pkey = xcsr.get_pubkey() - name = xcsr.get_subject().CN - return self.genHostCert(name, csr=pkey, signas=signas, outp=outp, sans=sans, save=save) - - def selfSignCert(self, cert, pkey): - ''' - Self-sign a certificate. - - Args: - cert (OpenSSL.crypto.X509): The certificate to sign. - pkey (OpenSSL.crypto.PKey): The PKey with which to sign the certificate. - - Examples: - Sign a given certificate with a given private key: - - cdir.selfSignCert(mycert, myotherprivatekey) - - Returns: - None - ''' - cert.set_issuer(cert.get_subject()) - cert.sign(pkey, self.signing_digest) - - def signUserCsr(self, xcsr, signas, outp=None, save=True): - ''' - Signs a user CSR with a CA keypair. - - Args: - xcsr (OpenSSL.crypto.X509Req): The certificate signing request. - signas (str): The CA keypair name to sign the CSR with. - outp (synapse.lib.output.Output): The output buffer. - - Examples: - cdir.signUserCsr(mycsr, 'myca') - - Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. - ''' - pkey = xcsr.get_pubkey() - name = xcsr.get_subject().CN - return self.genUserCert(name, csr=pkey, signas=signas, outp=outp, save=save) - - def _loadCasIntoSSLContext(self, ctx): - - for cdir in self.certdirs: - - path = s_common.genpath(cdir, 'cas') - if not os.path.isdir(path): - continue - - for name in os.listdir(path): - if name.endswith('.crt'): - ctx.load_verify_locations(os.path.join(path, name)) - - def getClientSSLContext(self, certname=None): - ''' - Returns an ssl.SSLContext appropriate for initiating a TLS session - - Args: - certname: If specified, use the user certificate with the matching - name to authenticate to the remote service. - Returns: - ssl.SSLContext: A SSLContext object. - ''' - sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) - sslctx.minimum_version = ssl.TLSVersion.TLSv1_2 - self._loadCasIntoSSLContext(sslctx) - - if certname is not None: - - username = certname - if username.find('@') != -1: - user, host = username.split('@', 1) - username = self.getUserForHost(user, host) - - if username is None: - mesg = f'User certificate not found: {certname}' - raise s_exc.NoSuchCert(mesg=mesg) - - certpath = self.getUserCertPath(username) - if certpath is None: - mesg = f'User certificate not found: {certname}' - raise s_exc.NoSuchCert(mesg=mesg) - - keypath = self.getUserKeyPath(username) - if keypath is None: - mesg = f'User private key not found: {certname}' - raise s_exc.NoCertKey(mesg=mesg) - - sslctx.load_cert_chain(certpath, keypath) - - return sslctx - - def getServerSSLContext(self, hostname=None, caname=None): - ''' - Returns an ssl.SSLContext appropriate to listen on a socket - - Args: - - hostname: If None, the value from socket.gethostname is used to find the key in the servers directory. - This name should match the not-suffixed part of two files ending in .key and .crt in the hosts - subdirectory. - - caname: If not None, the given name is used to locate a CA certificate used to validate client SSL certs. - - Returns: - ssl.SSLContext: A SSLContext object. - ''' - if hostname is not None and hostname.find(',') != -1: - # multi-hostname SNI routing has been requested - ctxs = {} - names = hostname.split(',') - for name in names: - ctxs[name] = self._getServerSSLContext(name, caname=caname) - - def snifunc(sslsock, sslname, origctx): - sslsock.context = ctxs.get(sslname, origctx) - return None - - sslctx = ctxs.get(names[0]) - sslctx.sni_callback = snifunc - return sslctx - - return self._getServerSSLContext(hostname=hostname, caname=caname) - - def getCrlPath(self, name): - for cdir in self.certdirs: - path = s_common.genpath(cdir, 'crls', '%s.crl' % name) - if os.path.isfile(path): - return path - - def genCrlPath(self, name): - path = self.getCrlPath(name) - if path is None: - s_common.gendir(self.certdirs[0], 'crls') - path = os.path.join(self.certdirs[0], 'crls', f'{name}.crl') - return path - - def genCaCrl(self, name): - ''' - Get the CRL for a given CA. - - Args: - name (str): The CA name. - - Returns: - CRL: The CRL object. - ''' - return CRL(self, name) - - def _getServerSSLContext(self, hostname=None, caname=None): - sslctx = getServerSSLContext() - - if hostname is None: - hostname = socket.gethostname() - - certfile = self.getHostCertPath(hostname) - if certfile is None: - mesg = f'Missing TLS certificate file for host: {hostname}' - raise s_exc.NoCertKey(mesg=mesg) - - keyfile = self.getHostKeyPath(hostname) - if keyfile is None: - mesg = f'Missing TLS key file for host: {hostname}' - raise s_exc.NoCertKey(mesg=mesg) - - sslctx.load_cert_chain(certfile, keyfile) - - if caname is not None: - cafile = self.getCaCertPath(caname) - sslctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED - sslctx.load_verify_locations(cafile=cafile) - - return sslctx - - def saveCertPem(self, cert, path): - ''' - Save a certificate in PEM format to a file outside the certdir. - ''' - with s_common.genfile(path) as fd: - fd.truncate(0) - fd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) - - def savePkeyPem(self, pkey, path): - ''' - Save a private key in PEM format to a file outside the certdir. - ''' - with s_common.genfile(path) as fd: - fd.truncate(0) - fd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - - def saveCaCertByts(self, byts): - cert = self._loadCertByts(byts) - name = cert.get_subject().CN - return self._saveCertTo(cert, 'cas', f'{name}.crt') - - def saveHostCertByts(self, byts): - cert = self._loadCertByts(byts) - name = cert.get_subject().CN - return self._saveCertTo(cert, 'hosts', f'{name}.crt') - - def saveUserCertByts(self, byts): - cert = self._loadCertByts(byts) - name = cert.get_subject().CN - return self._saveCertTo(cert, 'users', f'{name}.crt') - - def _checkDupFile(self, path): - if os.path.isfile(path): - raise s_exc.DupFileName(mesg=f'Duplicate file {path}', path=path) - - def _genBasePkeyCert(self, name, pkey=None): - - if pkey is None: - pkey = crypto.PKey() - pkey.generate_key(crypto.TYPE_RSA, self.crypto_numbits) - - cert = crypto.X509() - cert.set_pubkey(pkey) - cert.set_version(2) - - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(TEN_YEARS) # Certpairs are good for 10 years - - cert.set_serial_number(int(s_common.guid(), 16)) - cert.get_subject().CN = name - - return pkey, cert - - def _genPkeyCsr(self, name, mode, outp=None): - pkey = crypto.PKey() - pkey.generate_key(crypto.TYPE_RSA, self.crypto_numbits) - - xcsr = crypto.X509Req() - xcsr.get_subject().CN = name - - xcsr.set_pubkey(pkey) - xcsr.sign(pkey, self.signing_digest) - - keypath = self._savePkeyTo(pkey, mode, '%s.key' % name) - if outp is not None: - outp.printf('key saved: %s' % (keypath,)) - - csrpath = self._getPathJoin(mode, '%s.csr' % name) - self._checkDupFile(csrpath) - - byts = crypto.dump_certificate_request(crypto.FILETYPE_PEM, xcsr) - - with s_common.genfile(csrpath) as fd: - fd.truncate(0) - fd.write(byts) - - if outp is not None: - outp.printf('csr saved: %s' % (csrpath,)) - - return byts - - def _getCaPath(self, cert): - subj = cert.get_issuer() - return self.getCaCertPath(subj.CN) - - def _getPathBytes(self, path): - if path is None: - return None - return s_common.getbytes(path) - - def _getPathJoin(self, *paths): - return s_common.genpath(self.certdirs[0], *paths) - - def _loadCertPath(self, path): - byts = self._getPathBytes(path) - if byts: - return self._loadCertByts(byts) - - def loadCertByts(self, byts): - ''' - Load a X509 certificate from its PEM encoded bytes. - - Args: - byts (bytes): The PEM encoded bytes of the certificate. - - Returns: - OpenSSL.crypto.X509: The X509 certificate. - - Raises: - BadCertBytes: If the certificate bytes are invalid. - ''' - return self._loadCertByts(byts) - - def _loadCertByts(self, byts: bytes) -> crypto.X509: - try: - return crypto.load_certificate(crypto.FILETYPE_PEM, byts) - except crypto.Error as e: - # Unwrap pyopenssl's exception_from_error_queue - estr = '' - for argv in e.args: - if estr: # pragma: no cover - estr += ', ' - estr += ' '.join((arg for arg in argv[0] if arg)) - raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') - - def _loadCsrPath(self, path): - byts = self._getPathBytes(path) - if byts: - return self._loadCsrByts(byts) - - def _loadCsrByts(self, byts): - return crypto.load_certificate_request(crypto.FILETYPE_PEM, byts) - - def _loadKeyPath(self, path): - byts = self._getPathBytes(path) - if byts: - return crypto.load_privatekey(crypto.FILETYPE_PEM, byts) - - def _loadP12Path(self, path): - byts = self._getPathBytes(path) - if byts: - # This API is deprecrated by PyOpenSSL and will need to be rewritten if pyopenssl is - # updated from v21.x.x. The APIs that use this are not directly exposed via the - # easycert tool currently, and are only used in unit tests. - return crypto.load_pkcs12(byts) - - def _saveCertTo(self, cert, *paths): - path = self._getPathJoin(*paths) - self._checkDupFile(path) - - with s_common.genfile(path) as fd: - fd.truncate(0) - fd.write(self._certToByts(cert)) - - return path - - def _certToByts(self, cert): - return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - - def _pkeyToByts(self, pkey): - return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) - - def _savePkeyTo(self, pkey, *paths): - path = self._getPathJoin(*paths) - self._checkDupFile(path) - - with s_common.genfile(path) as fd: - fd.truncate(0) - fd.write(self._pkeyToByts(pkey)) - - return path - - def _saveP12To(self, cert, *paths): - path = self._getPathJoin(*paths) - self._checkDupFile(path) - - with s_common.genfile(path) as fd: - fd.truncate(0) - fd.write(cert.export()) - - return path - -certdirold = CertDirOld() -def getCertDir() -> CertDirOld: - ''' - Get the singleton CertDir instance. - - Returns: - CertDir: A certdir object. - ''' - return certdirold - -def addCertPathOld(path): - return certdirold.addCertPath(path) - -def delCertPathOld(path): - return certdirold.delCertPath(path) - -def getCertDirnOld() -> str: - ''' - Get the expanded default path used by the singleton CertDir instance. - - Returns: - str: The path string. - ''' - return s_common.genpath(defdir) - class CertDir: ''' Certificate loading/generation/signing utilities. @@ -2767,7 +1367,7 @@ def genCrlPath(self, name: str) -> str: path = os.path.join(self.certdirs[0], 'crls', f'{name}.crl') return path - def genCaCrl(self, name: str) -> CRLNew: + def genCaCrl(self, name: str) -> Crl: ''' Get the CRL for a given CA. @@ -2777,7 +1377,7 @@ def genCaCrl(self, name: str) -> CRLNew: Returns: CRL: The CRL object. ''' - return CRLNew(self, name) + return Crl(self, name) def _getServerSSLContext(self, hostname=None, caname=None) -> ssl.SSLContext: sslctx = getServerSSLContext() diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 8d8ab74254..ffa8e7b32a 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -664,604 +664,3 @@ async def test_certdir_codesign(self): with self.raises(s_exc.BadCertVerify) as cm: cdir.valCodeCert(byts) self.isin('certificate revoked', cm.exception.get('mesg')) - -class CertDirTest(s_t_utils.SynTest): - - def setUp(self) -> None: - self.skip(mesg='Skipping old tests.') - - @contextmanager - def getCertDir(self): - ''' - Get a test CertDir object. - - Yields: - s_certdir.CertDir: A certdir object based out of a temp directory. - ''' - # create a temp folder and make it a cert dir - with self.getTestDir() as dirname: - yield s_certdir.CertDir(path=dirname) - - def basic_assertions(self, cdir, cert, key, cacert=None): - ''' - test basic certificate assumptions - - Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - cacert (crypto.X509): Corresponding CA cert (optional) - ''' - self.nn(cert) - self.nn(key) - - # Make sure the certs were generated with the expected number of bits - self.eq(cert.get_pubkey().bits(), cdir.crypto_numbits) - self.eq(key.bits(), cdir.crypto_numbits) - - # Make sure the certs were generated with the correct version number - self.eq(cert.get_version(), 2) - - # ensure we can sign / verify data with our keypair - buf = b'The quick brown fox jumps over the lazy dog.' - sig = crypto.sign(key, buf, 'sha256') - sig2 = crypto.sign(key, buf + b'wut', 'sha256') - self.none(crypto.verify(cert, sig, buf, 'sha256')) - self.raises(crypto.Error, crypto.verify, cert, sig2, buf, 'sha256') - - # ensure that a ssl context using both cert/key match - sslcontext = SSL.Context(SSL.TLSv1_2_METHOD) - sslcontext.use_certificate(cert) - sslcontext.use_privatekey(key) - self.none(sslcontext.check_privatekey()) - - if cacert: - - # Make sure the cert was signed by the CA - self.eq(cert.get_issuer().der(), cacert.get_subject().der()) - - store = crypto.X509Store() - ctx = crypto.X509StoreContext(store, cert) - - # OpenSSL should NOT be able to verify the certificate if its CA is not loaded - store.add_cert(cert) - self.raises(crypto.X509StoreContextError, ctx.verify_certificate) # unable to get local issuer certificate - - # Generate a separate CA that did not sign the certificate - try: - cdir.genCaCert('otherca') - except s_exc.DupFileName: - pass - - # OpenSSL should NOT be able to verify the certificate if its CA is not loaded - store.add_cert(cdir.getCaCert('otherca')) - self.raises(crypto.X509StoreContextError, ctx.verify_certificate) # unable to get local issuer certificate - - # OpenSSL should be able to verify the certificate, once its CA is loaded - store.add_cert(cacert) - self.none(ctx.verify_certificate()) # valid - - def p12_assertions(self, cdir, cert, key, p12, cacert=None): - ''' - test basic p12 certificate bundle assumptions - - Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - p12 (crypto.PKCS12): PKCS12 object to test - cacert (crypto.X509): Corresponding CA cert (optional) - ''' - self.nn(p12) - - # Pull out the CA cert and keypair data - p12_cacert = None - if cacert: - p12_cacert = p12.get_ca_certificates() - self.nn(p12_cacert) - self.len(1, p12_cacert) - p12_cacert = p12_cacert[0] - self.eq(crypto.dump_certificate(crypto.FILETYPE_ASN1, cacert), crypto.dump_certificate(crypto.FILETYPE_ASN1, p12_cacert)) - - p12_cert = p12.get_certificate() - p12_key = p12.get_privatekey() - self.basic_assertions(cdir, p12_cert, p12_key, cacert=p12_cacert) - - # Make sure that the CA cert and keypair files are the same as the CA cert and keypair contained in the p12 file - self.eq(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert), crypto.dump_certificate(crypto.FILETYPE_ASN1, p12_cert)) - self.eq(crypto.dump_privatekey(crypto.FILETYPE_ASN1, key), crypto.dump_privatekey(crypto.FILETYPE_ASN1, p12_key)) - - def user_assertions(self, cdir, cert, key, cacert=None): - ''' - test basic certificate assumptions for a host certificate - - Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - cacert (crypto.X509): Corresponding CA cert (optional) - ''' - nextensions = cert.get_extension_count() - exts = {ext.get_short_name(): ext.get_data() for ext in [cert.get_extension(i) for i in range(nextensions)]} - - nscertext = crypto.X509Extension(b'nsCertType', False, b'client') - keyuseext = crypto.X509Extension(b'keyUsage', False, b'digitalSignature') - extkeyuseext = crypto.X509Extension(b'extendedKeyUsage', False, b'clientAuth') - basicconext = crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE') - self.eq(exts[b'nsCertType'], nscertext.get_data()) - self.eq(exts[b'keyUsage'], keyuseext.get_data()) - self.eq(exts[b'extendedKeyUsage'], extkeyuseext.get_data()) - self.eq(exts[b'basicConstraints'], basicconext.get_data()) - self.notin(b'subjectAltName', exts) - - def host_assertions(self, cdir, cert, key, cacert=None): - ''' - test basic certificate assumptions for a host certificate - - Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - cacert (crypto.X509): Corresponding CA cert (optional) - ''' - nextensions = cert.get_extension_count() - exts = {ext.get_short_name(): ext.get_data() for ext in [cert.get_extension(i) for i in range(nextensions)]} - - nscertext = crypto.X509Extension(b'nsCertType', False, b'server') - keyuseext = crypto.X509Extension(b'keyUsage', False, b'digitalSignature,keyEncipherment') - extkeyuseext = crypto.X509Extension(b'extendedKeyUsage', False, b'serverAuth') - basicconext = crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE') - - self.eq(exts[b'nsCertType'], nscertext.get_data()) - self.eq(exts[b'keyUsage'], keyuseext.get_data()) - self.eq(exts[b'extendedKeyUsage'], extkeyuseext.get_data()) - self.eq(exts[b'basicConstraints'], basicconext.get_data()) - self.isin(b'subjectAltName', exts) - - def test_certdir_cas(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - caname = 'syntest' - inter_name = 'testsyn-intermed' - base = cdir._getPathJoin() - - # Test that all the methods for loading the certificates return correct values for non-existant files - self.none(cdir.getCaCert(caname)) - self.none(cdir.getCaKey(caname)) - self.false(cdir.isCaCert(caname)) - self.none(cdir.getCaCertPath(caname)) - self.none(cdir.getCaKeyPath(caname)) - - # Generate a self-signed CA ======================================= - cdir.genCaCert(caname) - - # Test that all the methods for loading the certificates work - self.isinstance(cdir.getCaCert(caname), crypto.X509) - self.isinstance(cdir.getCaKey(caname), crypto.PKey) - self.true(cdir.isCaCert(caname)) - self.eq(cdir.getCaCertPath(caname), base + '/cas/' + caname + '.crt') - self.eq(cdir.getCaKeyPath(caname), base + '/cas/' + caname + '.key') - - # Run basic assertions on the CA keypair - cacert = cdir.getCaCert(caname) - cakey = cdir.getCaKey(caname) - self.basic_assertions(cdir, cacert, cakey) - - # Generate intermediate CA ======================================== - cdir.genCaCert(inter_name, signas=caname) - - # Run basic assertions, make sure that it was signed by the root CA - inter_cacert = cdir.getCaCert(inter_name) - inter_cakey = cdir.getCaKey(inter_name) - self.basic_assertions(cdir, inter_cacert, inter_cakey, cacert=cacert) - - def test_certdir_hosts(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - caname = 'syntest' - hostname = 'visi.vertex.link' - hostname_unsigned = 'unsigned.vertex.link' - base = cdir._getPathJoin() - - cdir.genCaCert(caname) - - cacert = cdir.getCaCert(caname) - - # Test that all the methods for loading the certificates return correct values for non-existant files - self.none(cdir.getHostCert(hostname_unsigned)) - self.none(cdir.getHostKey(hostname_unsigned)) - self.false(cdir.isHostCert(hostname_unsigned)) - self.none(cdir.getHostCertPath(hostname_unsigned)) - self.none(cdir.getHostKeyPath(hostname_unsigned)) - self.none(cdir.getHostCaPath(hostname_unsigned)) - - # Generate a self-signed host keypair ============================= - cdir.genHostCert(hostname_unsigned) - - # Test that all the methods for loading the certificates work - self.isinstance(cdir.getHostCert(hostname_unsigned), crypto.X509) - self.isinstance(cdir.getHostKey(hostname_unsigned), crypto.PKey) - self.true(cdir.isHostCert(hostname_unsigned)) - self.eq(cdir.getHostCertPath(hostname_unsigned), base + '/hosts/' + hostname_unsigned + '.crt') - self.eq(cdir.getHostKeyPath(hostname_unsigned), base + '/hosts/' + hostname_unsigned + '.key') - self.none(cdir.getHostCaPath(hostname_unsigned)) # the cert is self-signed, so there is no ca cert - - # Run basic assertions on the host keypair - cert = cdir.getHostCert(hostname_unsigned) - key = cdir.getHostKey(hostname_unsigned) - self.basic_assertions(cdir, cert, key) - self.host_assertions(cdir, cert, key) - - # Generate a signed host keypair ================================== - cdir.genHostCert(hostname, signas=caname) - - # Test that all the methods for loading the certificates work - self.isinstance(cdir.getHostCert(hostname), crypto.X509) - self.isinstance(cdir.getHostKey(hostname), crypto.PKey) - self.true(cdir.isHostCert(hostname)) - self.eq(cdir.getHostCertPath(hostname), base + '/hosts/' + hostname + '.crt') - self.eq(cdir.getHostKeyPath(hostname), base + '/hosts/' + hostname + '.key') - self.eq(cdir.getHostCaPath(hostname), base + '/cas/' + caname + '.crt') # the cert is signed, so there is a ca cert - - # Run basic assertions on the host keypair - cert = cdir.getHostCert(hostname) - key = cdir.getHostKey(hostname) - self.basic_assertions(cdir, cert, key, cacert=cacert) - self.host_assertions(cdir, cert, key, cacert=cacert) - - def test_certdir_users(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - caname = 'syntest' - username = 'visi@vertex.link' - username_unsigned = 'unsigned@vertex.link' - base = cdir._getPathJoin() - - cdir.genCaCert(caname) - cacert = cdir.getCaCert(caname) - - # Test that all the methods for loading the certificates return correct values for non-existant files - self.none(cdir.getUserCert(username_unsigned)) - self.none(cdir.getUserKey(username_unsigned)) - self.none(cdir.getClientCert(username_unsigned)) - self.false(cdir.isUserCert(username_unsigned)) - self.false(cdir.isClientCert(username_unsigned)) - self.none(cdir.getUserCertPath('nope')) - self.none(cdir.getUserKeyPath('nope')) - self.none(cdir.getUserCaPath('nope')) - self.none(cdir.getUserForHost('nope', 'host.vertex.link')) - - # Generate a self-signed user keypair ============================= - cdir.genUserCert(username_unsigned) - self.raises(s_exc.NoSuchFile, cdir.genClientCert, username_unsigned) - - # Test that all the methods for loading the certificates work - self.isinstance(cdir.getUserCert(username_unsigned), crypto.X509) - self.isinstance(cdir.getUserKey(username_unsigned), crypto.PKey) - self.none(cdir.getClientCert(username_unsigned)) - self.true(cdir.isUserCert(username_unsigned)) - self.false(cdir.isClientCert(username_unsigned)) - self.eq(cdir.getUserCertPath(username_unsigned), base + '/users/' + username_unsigned + '.crt') - self.eq(cdir.getUserKeyPath(username_unsigned), base + '/users/' + username_unsigned + '.key') - self.none(cdir.getUserCaPath(username_unsigned)) # no CA - self.eq(cdir.getUserForHost('unsigned', 'host.vertex.link'), username_unsigned) - - # Run basic assertions on the host keypair - cert = cdir.getUserCert(username_unsigned) - key = cdir.getUserKey(username_unsigned) - self.basic_assertions(cdir, cert, key) - self.user_assertions(cdir, cert, key) - - # Generate a signed user keypair ================================== - cdir.genUserCert(username, signas=caname) - cdir.genClientCert(username) - - # Test that all the methods for loading the certificates work - self.isinstance(cdir.getUserCert(username), crypto.X509) - self.isinstance(cdir.getUserKey(username), crypto.PKey) - self.isinstance(cdir.getClientCert(username), crypto.PKCS12) - self.true(cdir.isUserCert(username)) - self.true(cdir.isClientCert(username)) - self.eq(cdir.getUserCertPath(username), base + '/users/' + username + '.crt') - self.eq(cdir.getUserKeyPath(username), base + '/users/' + username + '.key') - self.eq(cdir.getUserCaPath(username), base + '/cas/' + caname + '.crt') - self.eq(cdir.getUserForHost('visi', 'host.vertex.link'), username) - - # Run basic assertions on the host keypair - cert = cdir.getUserCert(username) - key = cdir.getUserKey(username) - p12 = cdir.getClientCert(username) - self.basic_assertions(cdir, cert, key, cacert=cacert) - self.user_assertions(cdir, cert, key, cacert=cacert) - self.p12_assertions(cdir, cert, key, p12, cacert=cacert) - - # Test missing files for generating a client cert - os.remove(base + '/users/' + username + '.key') - self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user key - os.remove(base + '/cas/' + caname + '.crt') - self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # ca crt - os.remove(base + '/users/' + username + '.crt') - self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user crt - - def test_certdir_hosts_sans(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - caname = 'syntest' - cdir.genCaCert(caname) - - # Host cert with multiple SANs ==================================== - hostname = 'visi.vertex.link' - sans = 'DNS:vertex.link,DNS:visi.vertex.link,DNS:vertex.link' - cdir.genHostCert(hostname, signas=caname, sans=sans) - - cdir.getCaCert(caname) - cert = cdir.getHostCert(hostname) - cdir.getHostKey(hostname) - - self.eq(cert.get_extension_count(), 5) - self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') - self.eq(cert.get_extension(4).get_data(), b'0\x1f\x82\x0bvertex.link\x82\x10visi.vertex.link') # ASN.1 encoded subjectAltName data - - # Host cert with no specified SANs ================================ - hostname = 'visi2.vertex.link' - cdir.genHostCert(hostname, signas=caname) - - cdir.getCaCert(caname) - cert = cdir.getHostCert(hostname) - cdir.getHostKey(hostname) - - self.eq(cert.get_extension_count(), 5) - self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') - self.eq(cert.get_extension(4).get_data(), b'0\x13\x82\x11visi2.vertex.link') # ASN.1 encoded subjectAltName data - - # Self-signed Host cert with no specified SANs ==================== - hostname = 'visi3.vertex.link' - cdir.genHostCert(hostname) - - cdir.getCaCert(caname) - cert = cdir.getHostCert(hostname) - cdir.getHostKey(hostname) - - self.eq(cert.get_extension_count(), 5) - self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') - self.eq(cert.get_extension(4).get_data(), b'0\x13\x82\x11visi3.vertex.link') # ASN.1 encoded subjectAltName data - - def test_certdir_hosts_csr(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - caname = 'syntest' - hostname = 'visi.vertex.link' - - # Generate CA cert and host CSR - cdir.genCaCert(caname) - cdir.genHostCsr(hostname) - path = cdir._getPathJoin('hosts', hostname + '.csr') - xcsr = cdir._loadCsrPath(path) - - # Sign the CSR as the CA - pkey, pcert = cdir.signHostCsr(xcsr, caname) - self.isinstance(pkey, crypto.PKey) - self.isinstance(pcert, crypto.X509) - - # Validate the keypair - cacert = cdir.getCaCert(caname) - cert = cdir.getHostCert(hostname) - key = cdir.getHostKey(hostname) - self.basic_assertions(cdir, cert, key, cacert=cacert) - - def test_certdir_users_csr(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - caname = 'syntest' - username = 'visi@vertex.link' - - # Generate CA cert and user CSR - cdir.genCaCert(caname) - cdir.genUserCsr(username) - path = cdir._getPathJoin('users', username + '.csr') - xcsr = cdir._loadCsrPath(path) - - # Sign the CSR as the CA - pkey, pcert = cdir.signUserCsr(xcsr, caname) - self.isinstance(pkey, crypto.PKey) - self.isinstance(pcert, crypto.X509) - - # Validate the keypair - cacert = cdir.getCaCert(caname) - cert = cdir.getUserCert(username) - key = cdir.getUserKey(username) - self.basic_assertions(cdir, cert, key, cacert=cacert) - - def test_certdir_importfile(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - with self.getTestDir() as testpath: - - # File doesn't exist - fpath = s_common.genpath(testpath, 'not_real.crt') - self.raises(s_exc.NoSuchFile, cdir.importFile, fpath, 'cas') - - # File has unsupported extension - fpath = s_common.genpath(testpath, 'coolpic.bmp') - with s_common.genfile(fpath) as fd: - self.raises(s_exc.BadFileExt, cdir.importFile, fpath, 'cas') - - tests = ( - ('cas', 'coolca.crt'), - ('cas', 'coolca.key'), - ('hosts', 'coolhost.crt'), - ('hosts', 'coolhost.key'), - ('users', 'cooluser.crt'), - ('users', 'cooluser.key'), - ('users', 'cooluser.p12'), - ) - for ftype, fname in tests: - srcpath = s_common.genpath(testpath, fname) - dstpath = s_common.genpath(cdir.certdirs[0], ftype, fname) - - with s_common.genfile(srcpath) as fd: - fd.write(b'arbitrary data') - fd.seek(0) - - # Make sure the file is not there - self.raises(s_exc.NoSuchFile, s_common.reqfile, dstpath) - - # Import it and make sure it exists - self.none(cdir.importFile(srcpath, ftype)) - with s_common.reqfile(dstpath) as dstfd: - self.eq(dstfd.read(), b'arbitrary data') - - # Make sure it can't be overwritten - self.raises(s_exc.FileExists, cdir.importFile, srcpath, ftype) - - def test_certdir_valUserCert(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir - cdir._getPathJoin() - cdir.genCaCert('syntest') - cdir.genCaCert('newp') - cdir.getCaCerts() - syntestca = cdir.getCaCert('syntest') - newpca = cdir.getCaCert('newp') - - self.raises(s_exc.BadCertBytes, cdir.valUserCert, b'') - - cdir.genUserCert('cool') - path = cdir.getUserCertPath('cool') - byts = cdir._getPathBytes(path) - - self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts) - - cdir.genUserCert('cooler', signas='syntest') - path = cdir.getUserCertPath('cooler') - byts = cdir._getPathBytes(path) - self.nn(cdir.valUserCert(byts)) - self.nn(cdir.valUserCert(byts, cacerts=(syntestca,))) - self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=(newpca,)) - self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=()) - - cdir.genUserCert('coolest', signas='newp') - path = cdir.getUserCertPath('coolest') - byts = cdir._getPathBytes(path) - self.nn(cdir.valUserCert(byts)) - self.nn(cdir.valUserCert(byts, cacerts=(newpca,))) - self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=(syntestca,)) - self.raises(s_exc.BadCertVerify, cdir.valUserCert, byts, cacerts=()) - - def test_certdir_sslctx(self): - - with self.getCertDir() as cdir: - - with self.raises(s_exc.NoSuchCert): - cdir.getClientSSLContext(certname='newp') - - with s_common.genfile(cdir.certdirs[0], 'users', 'newp.crt') as fd: - fd.write(b'asdf') - - with self.raises(s_exc.NoCertKey): - cdir.getClientSSLContext(certname='newp') - - async def test_certdir_codesign(self): - - async with self.getTestCore() as core: - - caname = 'The Vertex Project ROOT CA' - immname = 'The Vertex Project Intermediate CA 00' - - codename = 'Vertex Build Pipeline' - - certpath = s_common.genpath(core.dirn, 'certs') - - core.certdir.genCaCert(caname) - core.certdir.genCaCert(immname, signas=caname) - core.certdir.genUserCert('notCodeCert', signas=caname, ) - - outp = s_output.OutPutStr() - self.eq(0, s_easycert.main(('--certdir', certpath, '--crl', caname), outp=outp)) - - outp = s_output.OutPutStr() - self.eq(0, s_easycert.main(('--certdir', certpath, '--crl', immname), outp=outp)) - - outp = s_output.OutPutStr() - self.eq(0, s_easycert.main(('--certdir', certpath, '--signas', immname, '--code', codename), outp=outp)) - - rsak = core.certdir.getCodeKey(codename) - cert = core.certdir.getCodeCert(codename) - - rsap = rsak.public() - - self.eq(rsak.iden(), rsap.iden()) - - sign = rsak.signitem({'foo': 'bar', 'baz': 'faz'}) - self.true(rsap.verifyitem({'baz': 'faz', 'foo': 'bar'}, sign)) - self.false(rsap.verifyitem({'baz': 'faz', 'foo': 'gronk'}, sign)) - - with self.getTestDir() as dirn: - - yamlpath = s_common.genpath(dirn, 'vertex-test.yaml') - jsonpath = s_common.genpath(dirn, 'vertex-test.json') - - s_common.yamlsave({ - 'name': 'vertex-test', - 'version': '0.0.1', - }, yamlpath) - - await s_genpkg.main(( - '--signas', codename, - '--certdir', certpath, - '--push', core.getLocalUrl(), '--push-verify', - yamlpath)) - - await s_genpkg.main(( - '--signas', codename, - '--certdir', certpath, - '--save', jsonpath, - yamlpath)) - - pkgdef = s_common.yamlload(jsonpath) - pkgorig = s_msgpack.deepcopy(pkgdef) - - opts = {'vars': {'pkgdef': pkgdef}} - self.none(await core.callStorm('return($lib.pkg.add($pkgdef, verify=$lib.true))', opts=opts)) - - with self.raises(s_exc.BadPkgDef) as exc: - pkgdef['version'] = '0.0.2' - await core.addStormPkg(pkgdef, verify=True) - self.eq(exc.exception.get('mesg'), 'Storm package signature does not match!') - - with self.raises(s_exc.BadPkgDef) as exc: - opts = {'vars': {'pkgdef': pkgdef}} - await core.callStorm('return($lib.pkg.add($pkgdef, verify=$lib.true))', opts=opts) - self.eq(exc.exception.get('mesg'), 'Storm package signature does not match!') - - with self.raises(s_exc.BadPkgDef) as exc: - pkgdef['codesign'].pop('sign', None) - await core.addStormPkg(pkgdef, verify=True) - self.eq(exc.exception.get('mesg'), 'Storm package has no signature!') - - with self.raises(s_exc.BadPkgDef) as exc: - pkgdef['codesign'].pop('cert', None) - await core.addStormPkg(pkgdef, verify=True) - self.eq(exc.exception.get('mesg'), 'Storm package has no certificate!') - - with self.raises(s_exc.BadPkgDef) as exc: - pkgdef.pop('codesign', None) - await core.addStormPkg(pkgdef, verify=True) - - self.eq(exc.exception.get('mesg'), 'Storm package is not signed!') - - with self.raises(s_exc.BadPkgDef) as exc: - await core.addStormPkg({'codesign': {'cert': 'foo', 'sign': 'bar'}}, verify=True) - self.eq(exc.exception.get('mesg'), 'Storm package has malformed certificate!') - - cert = '''-----BEGIN CERTIFICATE-----\nMIIE9jCCAt6''' - with self.raises(s_exc.BadPkgDef) as exc: - await core.addStormPkg({'codesign': {'cert': cert, 'sign': 'bar'}}, verify=True) - self.eq(exc.exception.get('mesg'), 'Storm package has malformed certificate!') - - usercertpath = core.certdir.getUserCertPath('notCodeCert') - with s_common.genfile(usercertpath) as fd: - cert = fd.read().decode() - with self.raises(s_exc.BadCertBytes) as exc: - await core.addStormPkg({'codesign': {'cert': cert, 'sign': 'bar'}}, verify=True) - self.eq(exc.exception.get('mesg'), 'Certificate is not for code signing.') - - # revoke our code signing cert and attempt to load - outp = s_output.OutPutStr() - self.eq(0, s_easycert.main(('--certdir', certpath, '--revokeas', immname, '--code', codename), outp=outp)) - - with self.raises(s_exc.BadPkgDef) as exc: - await core.addStormPkg(pkgorig, verify=True) - self.eq(exc.exception.get('mesg'), 'Storm package has invalid certificate: certificate revoked') From 27906dbed639ecbb2297c48303ee882f37fe554f Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 18:53:14 +0000 Subject: [PATCH 13/31] Correct types --- synapse/lib/certdir.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 6a3d4bb956..c515f3eb6d 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -100,11 +100,11 @@ def _unpackContextError(e: crypto.X509StoreContextError) -> str: class Crl: - def __init__(self, certdir, name): + def __init__(self, cdir, name): self.name = name - self.certdir = certdir - self.path = certdir.genCrlPath(name) + self.certdir = cdir + self.path = self.certdir.genCrlPath(name) self.crlbuilder = c_x509.CertificateRevocationListBuilder().issuer_name(c_x509.Name([ c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), @@ -506,7 +506,7 @@ def getCodeCertPath(self, name: str) -> StrOrNoneType: path = s_common.genpath(cdir, 'code', f'{name}.crt') if os.path.isfile(path): return path - def getCodeKey(self, name: str) -> s_rsa.PriKey: + def getCodeKey(self, name: str) -> Union[s_rsa.PriKey | None]: path = self.getCodeKeyPath(name) if path is None: @@ -612,10 +612,10 @@ def genClientCert(self, name: str, outp: OutPutOrNoneType =None) -> None: raise s_exc.NoSuchFile(mesg='missing User private key', name=name) byts = c_pkcs12.serialize_key_and_certificates(name=name.encode('utf-8'), - key=ukey, - cert=ucert, - cas=[cacert], - encryption_algorithm=c_serialization.NoEncryption()) + key=ukey, + cert=ucert, + cas=[cacert], + encryption_algorithm=c_serialization.NoEncryption()) crtpath = self._saveP12To(byts, 'users', '%s.p12' % name) if outp is not None: outp.printf('client cert saved: %s' % (crtpath,)) @@ -1003,7 +1003,7 @@ def getUserForHost(self, user: str, host: str) -> StrOrNoneType: if self.isUserCert(usercert): return usercert - def getUserKey(self, name: str) -> CertOrNoneType: + def getUserKey(self, name: str) -> PkeyOrNoneType: ''' Loads the PKey object for a given user keypair. @@ -1547,7 +1547,7 @@ def _loadCsrPath(self, path: str) -> Union[c_x509.CertificateSigningRequest | No if byts: return self._loadCsrByts(byts) - def _loadCsrByts(self, byts) -> bytes: + def _loadCsrByts(self, byts: bytes) -> c_x509.CertificateSigningRequest: return c_x509.load_pem_x509_csr(byts) def _loadKeyPath(self, path: str) -> PkeyOrNoneType: From cae4c767e449e2237076c256ad53f089103d26e2 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 19:22:41 +0000 Subject: [PATCH 14/31] Add missing typehint --- synapse/lib/certdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index c515f3eb6d..e11b1ebb60 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -1596,7 +1596,7 @@ def _savePkeyTo(self, pkey: PkeyType, *paths: str): return path - def _saveP12To(self, byts, *paths): + def _saveP12To(self, byts: bytes, *paths: str): path = self._getPathJoin(*paths) self._checkDupFile(path) From 66292ea1ca6523bac13a7e63b4085277200fc227 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Tue, 13 Feb 2024 19:57:23 +0000 Subject: [PATCH 15/31] Cleanup some other uses of pyopenssl --- synapse/lib/link.py | 10 +++++----- synapse/tests/test_lib_cell.py | 12 ++++++++---- synapse/tests/test_lib_certdir.py | 25 +++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/synapse/lib/link.py b/synapse/lib/link.py index 4cee1ce463..3300ab2491 100644 --- a/synapse/lib/link.py +++ b/synapse/lib/link.py @@ -1,11 +1,11 @@ import socket import asyncio import logging - -from OpenSSL import crypto - import collections +import cryptography.x509 as c_x509 +import cryptography.hazmat.primitives.hashes as c_hashes + logger = logging.getLogger(__name__) import synapse.exc as s_exc @@ -156,8 +156,8 @@ async def fini(): if self.certhash is not None: byts = info.get('ssl').telessl.getpeercert(True) - cert = crypto.load_certificate(crypto.FILETYPE_ASN1, byts) - thishash = cert.digest('SHA256').decode().lower().replace(':', '') + cert = c_x509.load_der_x509_certificate(byts) + thishash = s_common.ehex(cert.fingerprint(c_hashes.SHA256())) if thishash != self.certhash: mesg = f'Server cert does not match pinned certhash={self.certhash}.' raise s_exc.LinkBadCert(mesg=mesg) diff --git a/synapse/tests/test_lib_cell.py b/synapse/tests/test_lib_cell.py index af7cdbbfea..962689625f 100644 --- a/synapse/tests/test_lib_cell.py +++ b/synapse/tests/test_lib_cell.py @@ -12,6 +12,8 @@ from unittest import mock +import cryptography.x509 as c_x509 + import synapse.exc as s_exc import synapse.axon as s_axon import synapse.common as s_common @@ -2356,8 +2358,9 @@ def get_pem_cert(): return ssl.DER_cert_to_PEM_cert(der_cert) original_cert = await s_coro.executor(get_pem_cert) - ocert = crypto.load_certificate(crypto.FILETYPE_PEM, original_cert) - self.eq(ocert.get_subject().CN, 'reloadcell') + ocert = c_x509.load_pem_x509_certificate(original_cert.encode()) + cname = ocert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value + self.eq(cname, 'reloadcell') # Start a beholder session that runs over TLS @@ -2402,8 +2405,9 @@ async def beholdConsumer(): await cell.reload() reloaded_cert = await s_coro.executor(get_pem_cert) - rcert = crypto.load_certificate(crypto.FILETYPE_PEM, reloaded_cert) - self.eq(rcert.get_subject().CN, 'SomeTestCertificate') + rcert = c_x509.load_pem_x509_certificate(reloaded_cert.encode()) + rname = rcert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value + self.eq(rname, 'SomeTestCertificate') async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess: resp = await sess.get(f'https://localhost:{hport}/api/v1/healthcheck') diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index ffa8e7b32a..24fde83ae0 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -627,6 +627,8 @@ async def test_certdir_codesign(self): caname = 'The Vertex Project ROOT CA' immname = 'The Vertex Project Intermediate CA 00' codename = 'Vertex Build Pipeline' + codename2 = 'Vertex Build Pipeline Redux' + codename3 = 'Vertex Build Pipeline Triple Threat' cdir.genCaCert(caname) cdir.genCaCert(immname, signas=caname) @@ -664,3 +666,26 @@ async def test_certdir_codesign(self): with self.raises(s_exc.BadCertVerify) as cm: cdir.valCodeCert(byts) self.isin('certificate revoked', cm.exception.get('mesg')) + + # Ensure we can continue to revoke certs and old certs stay revoked. + _, codecert2 = cdir.genCodeCert(codename2, signas=immname) + _, codecert3 = cdir.genCodeCert(codename3, signas=immname) + + crl = cdir.genCaCrl(immname) + crl.revoke(codecert2) + + fp = cdir.getCodeCertPath(codename2) + with s_common.genfile(fp) as fd: + byts2 = fd.read() + + fp = cdir.getCodeCertPath(codename3) + with s_common.genfile(fp) as fd: + byts3 = fd.read() + + with self.raises(s_exc.BadCertVerify) as cm: + cdir.valCodeCert(byts) + + with self.raises(s_exc.BadCertVerify) as cm: + cdir.valCodeCert(byts2) + + cdir.valCodeCert(byts3) From d1ef0cd5a0fe6fedaa02edd6674572c57a62b74a Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 14 Feb 2024 00:43:28 +0000 Subject: [PATCH 16/31] Some more assertion tests and SANS support for host certificates. --- synapse/lib/certdir.py | 71 +++++++++------- synapse/tests/test_lib_certdir.py | 131 ++++++++++++++++++++---------- 2 files changed, 126 insertions(+), 76 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index e11b1ebb60..1f2151048c 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -32,6 +32,8 @@ logger = logging.getLogger(__name__) +NSTYPE_OID = '2.16.840.1.113730.1.1' + TEN_YEARS = 10 * 365 * 24 * 60 * 60 # 10 years in seconds TEN_YEARS_TD = datetime.timedelta(seconds=TEN_YEARS) @@ -285,10 +287,10 @@ def genCaCert(self, name: str, return prvkey, cert def genHostCert(self, name: str, - signas: Optional[str | None] = None, + signas: StrOrNoneType = None, outp: OutPutOrNoneType = None, csr: PubKeyOrNone =None, - sans: List[str] =None, + sans: StrOrNoneType =None, save: bool = True) -> PkeyOrNoneAndCertType: ''' Generates a host keypair. @@ -317,27 +319,45 @@ def genHostCert(self, name: str, builder = self._genCertBuilder(name, pubkey) - # XXX FIXME - Sort out some generic SANS support from pyopenssl loose apis - # ext_sans = {'DNS:' + name} - # if isinstance(sans, str): - # ext_sans = ext_sans.union(sans.split(',')) - # ext_sans = [c_x509.GeneralName(valu) for valu in sorted(ext_sans)] + ext_sans = collections.defaultdict(set) + ext_sans['dns'].add(name) + sans_ctors = {'dns': c_x509.DNSName, + 'email': c_x509.RFC822Name, + 'uri': c_x509.UniformResourceIdentifier} + if sans: + sans = sans.split(',') + for san in sans: + if san.startswith('DNS:'): + san = san[4:] + ext_sans['dns'].add(san) + elif san.startswith('email:'): + san = san[6:] + ext_sans['email'].add(san) + elif san.startswith('URI:'): + san = san[4:] + ext_sans['uri'].add(san) + else: + raise s_exc.BadArg(mesg=f'Unsupported san value: {san}') + sans = [] + for key, ctor in sans_ctors.items(): + values = sorted(ext_sans[key]) + for valu in values: + sans.append(ctor(valu)) - builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) - builder = builder.add_extension( - c_x509.KeyUsage(digital_signature=True, key_encipherment=True, data_encipherment=False, key_agreement=False, - key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, - content_commitment=False), + builder = builder.add_extension(c_x509.UnrecognizedExtension( + oid=c_x509.ObjectIdentifier(NSTYPE_OID), value=b'\x03\x02\x06@'), critical=False, ) - builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), critical=False) - builder = builder.add_extension(c_x509.UnrecognizedExtension( - oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@'), + builder = builder.add_extension( + c_x509.KeyUsage(digital_signature=True, key_encipherment=True, data_encipherment=False, + key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, + decipher_only=False, content_commitment=False), critical=False, ) - - # XXX FIXME - Sort out some generic SANS support from pyopenssl loose apis - # builder = builder.add_extension(c_x509.SubjectAlternativeName(ext_sans), critical=False) + builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), + critical=False) + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) + builder = builder.add_extension(c_x509.SubjectAlternativeName(sans), critical=False) if signas is not None: cert = self.signCertAs(builder, signas) @@ -1197,8 +1217,8 @@ def signCertAs(self, builder: c_x509.CertificateBuilder, signas: str) -> c_x509. def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, signas: str, - outp: OutPutOrNoneType=None, - sans=None, + outp: OutPutOrNoneType =None, + sans: StrOrNoneType =None, save: bool =True) -> PkeyOrNoneAndCertType: ''' Signs a host CSR with a CA keypair. @@ -1530,18 +1550,8 @@ def _loadCertByts(self, byts: bytes) -> c_x509.Certificate: try: return c_x509.load_pem_x509_certificate(byts) except Exception as e: - logger.exception('UNKNOWN EXCEPTION READING BYTES!') raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {e}') from None - # except crypto.Error as e: - # # Unwrap pyopenssl's exception_from_error_queue - # estr = '' - # for argv in e.args: - # if estr: # pragma: no cover - # estr += ', ' - # estr += ' '.join((arg for arg in argv[0] if arg)) - # raise s_exc.BadCertBytes(mesg=f'Failed to load bytes: {estr}') - def _loadCsrPath(self, path: str) -> Union[c_x509.CertificateSigningRequest | None]: byts = self._getPathBytes(path) if byts: @@ -1554,7 +1564,6 @@ def _loadKeyPath(self, path: str) -> PkeyOrNoneType: byts = self._getPathBytes(path) if byts: pkey = c_serialization.load_pem_private_key(byts, password=None) - # XXX FIXME Coverage for someone handling in a DER key? if isinstance(pkey, (c_rsa.RSAPrivateKey, c_dsa.DSAPrivateKey)): return pkey # XXX FIXME Coverage for this! diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 24fde83ae0..dbc541b2d1 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -27,7 +27,7 @@ import cryptography.hazmat.primitives.serialization.pkcs12 as c_pkcs12 -class CertDirNewTest(s_t_utils.SynTest): +class CertDirTest(s_t_utils.SynTest): @contextmanager def getCertDir(self): @@ -158,30 +158,36 @@ def host_assertions(self, key (crypto.PKey): Key for the certification cacert (crypto.X509): Corresponding CA cert (optional) ''' - # XXX FIXME There is a schism between teh items build for use with the builder - # interface nd the items parsed from a certificate on disk :\ - # exts = {} - # for ext in cert.extensions: # type: c_x509.Extension - # self.false(ext.critical) - # short_name = ext.oid._name - # if short_name == 'Unknown OID': - # short_name = ext.oid.dotted_string - # exts[short_name] = ext - # - # nscertext = c_x509.UnrecognizedExtension( - # oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@') - # keyuseext = c_x509.KeyUsage(digital_signature=True, key_encipherment=True, data_encipherment=False, key_agreement=False, - # key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, - # content_commitment=False) - # print(keyuseext.public_bytes()) - # extkeyuseext = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]) - # basicconext = c_x509.BasicConstraints(ca=False, path_length=None) - # - # # self.eq(exts['2.16.840.1.113730.1.1'], nscertext) - # self.eq(exts['keyUsage'], keyuseext) - # # self.eq(exts[b'extendedKeyUsage'], extkeyuseext.get_data()) - # # self.eq(exts[b'basicConstraints'], basicconext.get_data()) - # # self.isin(b'subjectAltName', exts) + + reqbc = c_x509.BasicConstraints(ca=False, path_length=None) + reqeku = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]) + reqku = c_x509.KeyUsage(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) + reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID), value=b'\x03\x02\x06@') + + bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) + self.eq(reqbc, bc.value) + + ku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.KEY_USAGE) + self.eq(reqku, ku.value) + + eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) + self.eq(reqeku, eku.value) + + nstype = cert.extensions.get_extension_for_oid(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID)) + self.eq(reqnstype, nstype.value) + + expected_oids = sorted([ + c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS.dotted_string, + c_x509.oid.ExtensionOID.KEY_USAGE.dotted_string, + c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE.dotted_string, + c_x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME.dotted_string, + s_certdir.NSTYPE_OID, + ]) + + ext_oids = sorted([ext.oid.dotted_string for ext in cert.extensions]) + self.eq(expected_oids, ext_oids) def user_assertions(self, cdir: s_certdir.CertDir, @@ -197,20 +203,36 @@ def user_assertions(self, key (crypto.PKey): Key for the certification cacert (crypto.X509): Corresponding CA cert (optional) ''' + reqbc = c_x509.BasicConstraints(ca=False, path_length=None) + reqeku = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]) + reqku = c_x509.KeyUsage(digital_signature=True, content_commitment=False, key_encipherment=False, + data_encipherment=False, key_agreement=False, key_cert_sign=False, + crl_sign=False, encipher_only=False, decipher_only=False) + + bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) + self.eq(reqbc, bc.value) + + ku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.KEY_USAGE) + self.eq(reqku, ku.value) + + eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) + self.eq(reqeku, eku.value) + + expected_oids = sorted([ + c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS.dotted_string, + c_x509.oid.ExtensionOID.KEY_USAGE.dotted_string, + c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE.dotted_string, + # XXX FIXME Add client oid string + # s_certdir.NSTYPE_OID, + ]) + + ext_oids = sorted([ext.oid.dotted_string for ext in cert.extensions]) + self.eq(expected_oids, ext_oids) + # XXX FIXME There is a schism between teh items build for use with the builder - # interface nd the items parsed from a certificate on disk :\ - # nextensions = cert.get_extension_count() - # exts = {ext.get_short_name(): ext.get_data() for ext in [cert.get_extension(i) for i in range(nextensions)]} # # nscertext = crypto.X509Extension(b'nsCertType', False, b'client') - # keyuseext = crypto.X509Extension(b'keyUsage', False, b'digitalSignature') - # extkeyuseext = crypto.X509Extension(b'extendedKeyUsage', False, b'clientAuth') - # basicconext = crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE') # self.eq(exts[b'nsCertType'], nscertext.get_data()) - # self.eq(exts[b'keyUsage'], keyuseext.get_data()) - # self.eq(exts[b'extendedKeyUsage'], extkeyuseext.get_data()) - # self.eq(exts[b'basicConstraints'], basicconext.get_data()) - # self.notin(b'subjectAltName', exts) def p12_assertions(self, cdir: s_certdir.CertDir, @@ -428,7 +450,6 @@ def test_certdir_users(self): self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user crt def test_certdir_hosts_sans(self): - self.skip('XXX FIXME Sort out SANS support.') with self.getCertDir() as cdir: # type: s_certdir.CertDir caname = 'syntest' cdir.genCaCert(caname) @@ -442,9 +463,9 @@ def test_certdir_hosts_sans(self): cert = cdir.getHostCert(hostname) cdir.getHostKey(hostname) - self.eq(cert.get_extension_count(), 5) - self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') - self.eq(cert.get_extension(4).get_data(), b'0\x1f\x82\x0bvertex.link\x82\x10visi.vertex.link') # ASN.1 encoded subjectAltName data + ext = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + self.len(2, ext.value) + self.eq(ext.value.get_values_for_type(c_x509.DNSName), ['vertex.link', 'visi.vertex.link']) # Host cert with no specified SANs ================================ hostname = 'visi2.vertex.link' @@ -454,9 +475,9 @@ def test_certdir_hosts_sans(self): cert = cdir.getHostCert(hostname) cdir.getHostKey(hostname) - self.eq(cert.get_extension_count(), 5) - self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') - self.eq(cert.get_extension(4).get_data(), b'0\x13\x82\x11visi2.vertex.link') # ASN.1 encoded subjectAltName data + ext = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + self.len(1, ext.value) + self.eq(ext.value.get_values_for_type(c_x509.DNSName), ['visi2.vertex.link']) # Self-signed Host cert with no specified SANs ==================== hostname = 'visi3.vertex.link' @@ -466,9 +487,29 @@ def test_certdir_hosts_sans(self): cert = cdir.getHostCert(hostname) cdir.getHostKey(hostname) - self.eq(cert.get_extension_count(), 5) - self.eq(cert.get_extension(4).get_short_name(), b'subjectAltName') - self.eq(cert.get_extension(4).get_data(), b'0\x13\x82\x11visi3.vertex.link') # ASN.1 encoded subjectAltName data + ext = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + self.len(1, ext.value) + self.eq(ext.value.get_values_for_type(c_x509.DNSName), ['visi3.vertex.link']) + + # Backwards compatibility with pyopenssl sans specifiers which we can get from easycert + hostname = 'stuff.vertex.link' + sans = 'DNS:wow.com,email:clown@vertex.link,URI:https://hehe.haha.vertex.link,email:hehe@vertex.link' + cdir.genHostCert(hostname, signas=caname, sans=sans) + + cdir.getCaCert(caname) + cert = cdir.getHostCert(hostname) + cdir.getHostKey(hostname) + + ext = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + self.len(5, ext.value) + self.eq(ext.value.get_values_for_type(c_x509.DNSName), ['stuff.vertex.link', 'wow.com']) + self.eq(ext.value.get_values_for_type(c_x509.RFC822Name), ['clown@vertex.link', 'hehe@vertex.link']) + self.eq(ext.value.get_values_for_type(c_x509.UniformResourceIdentifier), ['https://hehe.haha.vertex.link']) + + hostname = 'newp.vertex.link' + sans = 'DNS:wow.com,email:clown@vertex.link,HAHA:yeahRight!' + with self.raises(s_exc.BadArg): + cdir.genHostCert(hostname, signas=caname, sans=sans) def test_certdir_hosts_csr(self): with self.getCertDir() as cdir: # type: s_certdir.CertDir From e8164e8ce6b6daf5cbd228af34e232ba1de941f8 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 14 Feb 2024 01:11:44 +0000 Subject: [PATCH 17/31] nsCertType support --- synapse/lib/certdir.py | 36 ++++++++++++++----------------- synapse/tests/test_lib_certdir.py | 20 ++++++++--------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 1f2151048c..5bc4dd6b57 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -32,7 +32,10 @@ logger = logging.getLogger(__name__) -NSTYPE_OID = '2.16.840.1.113730.1.1' +NSCERTTYPE_OID = '2.16.840.1.113730.1.1' +NSCERTTYPE_CLIENT = b'\x03\x02\x07\x80' # client +NSCERTTYPE_SERVER = b'\x03\x02\x06@' # server +NSCERTTYPE_OBJSIGN = b'\x03\x02\x04\x10' # objsign TEN_YEARS = 10 * 365 * 24 * 60 * 60 # 10 years in seconds TEN_YEARS_TD = datetime.timedelta(seconds=TEN_YEARS) @@ -345,7 +348,7 @@ def genHostCert(self, name: str, sans.append(ctor(valu)) builder = builder.add_extension(c_x509.UnrecognizedExtension( - oid=c_x509.ObjectIdentifier(NSTYPE_OID), value=b'\x03\x02\x06@'), + oid=c_x509.ObjectIdentifier(NSCERTTYPE_OID), value=NSCERTTYPE_SERVER), critical=False, ) builder = builder.add_extension( @@ -425,8 +428,10 @@ def genUserCert(self, pubkey = csr builder = self._genCertBuilder(name, pubkey) - - builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) + builder = builder.add_extension(c_x509.UnrecognizedExtension( + oid=c_x509.ObjectIdentifier(NSCERTTYPE_OID), value=NSCERTTYPE_CLIENT), + critical=False, + ) builder = builder.add_extension( c_x509.KeyUsage(digital_signature=True, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, @@ -435,12 +440,7 @@ def genUserCert(self, ) builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]), critical=False) - # XXX FIXME Client encoding value for - # crypto.X509Extension(b'nsCertType', False, b'client'), - # builder = builder.add_extension(c_x509.UnrecognizedExtension( - # oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@'), - # critical=False, - # ) + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) if signas is not None: cert = self.signCertAs(builder, signas) @@ -482,8 +482,10 @@ def genCodeCert(self, name: str, signas: StrOrNoneType =None, outp: OutPutOrNone pubkey = prvkey.public_key() builder = self._genCertBuilder(name, pubkey) - - builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) + builder = builder.add_extension(c_x509.UnrecognizedExtension( + oid=c_x509.ObjectIdentifier(NSCERTTYPE_OID), value=NSCERTTYPE_OBJSIGN), + critical=False, + ) builder = builder.add_extension( c_x509.KeyUsage(digital_signature=True, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, @@ -492,12 +494,7 @@ def genCodeCert(self, name: str, signas: StrOrNoneType =None, outp: OutPutOrNone ) builder = builder.add_extension(c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]), critical=False) - # XXX FIXME Client encoding value for - # crypto.X509Extension(b'nsCertType', False, b'objsign'), - # builder = builder.add_extension(c_x509.UnrecognizedExtension( - # oid=c_x509.ObjectIdentifier('2.16.840.1.113730.1.1'), value=b'\x03\x02\x06@'), - # critical=False, - # ) + builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=False) if signas is not None: cert = self.signCertAs(builder, signas) @@ -557,7 +554,6 @@ def valCodeCert(self, byts: bytes) -> c_x509.Certificate: cert = self.loadCertByts(byts) eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) - # XXX FIXME if eku is none??/ if reqext != eku.value: mesg = 'Certificate is not for code signing.' raise s_exc.BadCertBytes(mesg=mesg) @@ -580,7 +576,7 @@ def valCodeCert(self, byts: bytes) -> c_x509.Certificate: raise s_exc.BadCertVerify(mesg=mesg) return cert - def _getCaCrls(self): + def _getCaCrls(self) -> List[c_x509.CertificateRevocationList]: crls = [] for cdir in self.certdirs: diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index dbc541b2d1..f413c71a1d 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -164,7 +164,8 @@ def host_assertions(self, reqku = c_x509.KeyUsage(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) - reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID), value=b'\x03\x02\x06@') + reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID), + value=s_certdir.NSCERTTYPE_SERVER) bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) self.eq(reqbc, bc.value) @@ -175,7 +176,7 @@ def host_assertions(self, eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) self.eq(reqeku, eku.value) - nstype = cert.extensions.get_extension_for_oid(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID)) + nstype = cert.extensions.get_extension_for_oid(c_x509.ObjectIdentifier(s_certdir.NSCERTTYPE_OID)) self.eq(reqnstype, nstype.value) expected_oids = sorted([ @@ -183,7 +184,7 @@ def host_assertions(self, c_x509.oid.ExtensionOID.KEY_USAGE.dotted_string, c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE.dotted_string, c_x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME.dotted_string, - s_certdir.NSTYPE_OID, + s_certdir.NSCERTTYPE_OID, ]) ext_oids = sorted([ext.oid.dotted_string for ext in cert.extensions]) @@ -208,6 +209,8 @@ def user_assertions(self, reqku = c_x509.KeyUsage(digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False) + reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID), + value=s_certdir.NSCERTTYPE_CLIENT) bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) self.eq(reqbc, bc.value) @@ -218,22 +221,19 @@ def user_assertions(self, eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) self.eq(reqeku, eku.value) + nstype = cert.extensions.get_extension_for_oid(c_x509.ObjectIdentifier(s_certdir.NSCERTTYPE_OID)) + self.eq(reqnstype, nstype.value) + expected_oids = sorted([ c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS.dotted_string, c_x509.oid.ExtensionOID.KEY_USAGE.dotted_string, c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE.dotted_string, - # XXX FIXME Add client oid string - # s_certdir.NSTYPE_OID, + s_certdir.NSCERTTYPE_OID, ]) ext_oids = sorted([ext.oid.dotted_string for ext in cert.extensions]) self.eq(expected_oids, ext_oids) - # XXX FIXME There is a schism between teh items build for use with the builder - # - # nscertext = crypto.X509Extension(b'nsCertType', False, b'client') - # self.eq(exts[b'nsCertType'], nscertext.get_data()) - def p12_assertions(self, cdir: s_certdir.CertDir, cert: c_x509.Certificate, From f44cf4ba8f6bf77ffd2d2cc23ebef93b518bec6f Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 14 Feb 2024 16:37:55 +0000 Subject: [PATCH 18/31] Fix test --- synapse/tests/test_lib_certdir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index f413c71a1d..00e0b8f0f0 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -164,7 +164,7 @@ def host_assertions(self, reqku = c_x509.KeyUsage(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) - reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID), + reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSCERTTYPE_OID), value=s_certdir.NSCERTTYPE_SERVER) bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) @@ -209,7 +209,7 @@ def user_assertions(self, reqku = c_x509.KeyUsage(digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False) - reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSTYPE_OID), + reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSCERTTYPE_OID), value=s_certdir.NSCERTTYPE_CLIENT) bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) From 7243b07a5289720e1aaab5bb4db565e85d583772 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 14 Feb 2024 17:49:37 +0000 Subject: [PATCH 19/31] Restore cortex codesign / package loading tests --- synapse/cortex.py | 4 +- synapse/tests/test_lib_certdir.py | 100 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/synapse/cortex.py b/synapse/cortex.py index adf5b5544d..e60fb5f323 100644 --- a/synapse/cortex.py +++ b/synapse/cortex.py @@ -2451,7 +2451,7 @@ async def addStormPkg(self, pkgdef, verify=False): raise s_exc.BadPkgDef(mesg=mesg) try: - cert = self.certdir.loadCertByts(certbyts) + cert = self.certdir.loadCertByts(certbyts.encode('utf-8')) except s_exc.BadCertBytes as e: raise s_exc.BadPkgDef(mesg='Storm package has malformed certificate!') from None @@ -2465,7 +2465,7 @@ async def addStormPkg(self, pkgdef, verify=False): mesg = 'Storm package has invalid certificate!' raise s_exc.BadPkgDef(mesg=mesg) from None - pubk = s_rsa.PubKey(cert.get_pubkey().to_cryptography_key()) + pubk = s_rsa.PubKey(cert.public_key()) if not pubk.verifyitem(pkgcopy, s_common.uhex(signbyts)): mesg = 'Storm package signature does not match!' raise s_exc.BadPkgDef(mesg=mesg) diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 00e0b8f0f0..94f33f91d6 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -730,3 +730,103 @@ async def test_certdir_codesign(self): cdir.valCodeCert(byts2) cdir.valCodeCert(byts3) + + async def test_cortex_codesign(self): + + async with self.getTestCore() as core: + + caname = 'Test ROOT CA' + immname = 'Test Intermediate CA 00' + codename = 'Test Build Pipeline' + + certpath = s_common.genpath(core.dirn, 'certs') + + core.certdir.genCaCert(caname) + core.certdir.genCaCert(immname, signas=caname) + core.certdir.genUserCert('notCodeCert', signas=caname, ) + + crl = core.certdir.genCaCrl(caname) + crl._save() + + crl = core.certdir.genCaCrl(immname) + crl._save() + + _, codecert = core.certdir.genCodeCert(codename, signas=immname) + + with self.getTestDir() as dirn: + + yamlpath = s_common.genpath(dirn, 'vertex-test.yaml') + jsonpath = s_common.genpath(dirn, 'vertex-test.json') + + s_common.yamlsave({ + 'name': 'vertex-test', + 'version': '0.0.1', + }, yamlpath) + + await s_genpkg.main(( + '--signas', codename, + '--certdir', certpath, + '--push', core.getLocalUrl(), '--push-verify', + yamlpath)) + + await s_genpkg.main(( + '--signas', codename, + '--certdir', certpath, + '--save', jsonpath, + yamlpath)) + + pkgdef = s_common.yamlload(jsonpath) + pkgorig = s_msgpack.deepcopy(pkgdef) + + opts = {'vars': {'pkgdef': pkgdef}} + self.none(await core.callStorm('return($lib.pkg.add($pkgdef, verify=$lib.true))', opts=opts)) + + with self.raises(s_exc.BadPkgDef) as exc: + pkgdef['version'] = '0.0.2' + await core.addStormPkg(pkgdef, verify=True) + self.eq(exc.exception.get('mesg'), 'Storm package signature does not match!') + + with self.raises(s_exc.BadPkgDef) as exc: + opts = {'vars': {'pkgdef': pkgdef}} + await core.callStorm('return($lib.pkg.add($pkgdef, verify=$lib.true))', opts=opts) + self.eq(exc.exception.get('mesg'), 'Storm package signature does not match!') + + with self.raises(s_exc.BadPkgDef) as exc: + pkgdef['codesign'].pop('sign', None) + await core.addStormPkg(pkgdef, verify=True) + self.eq(exc.exception.get('mesg'), 'Storm package has no signature!') + + with self.raises(s_exc.BadPkgDef) as exc: + pkgdef['codesign'].pop('cert', None) + await core.addStormPkg(pkgdef, verify=True) + self.eq(exc.exception.get('mesg'), 'Storm package has no certificate!') + + with self.raises(s_exc.BadPkgDef) as exc: + pkgdef.pop('codesign', None) + await core.addStormPkg(pkgdef, verify=True) + + self.eq(exc.exception.get('mesg'), 'Storm package is not signed!') + + with self.raises(s_exc.BadPkgDef) as exc: + await core.addStormPkg({'codesign': {'cert': 'foo', 'sign': 'bar'}}, verify=True) + self.eq(exc.exception.get('mesg'), 'Storm package has malformed certificate!') + + cert = '''-----BEGIN CERTIFICATE-----\nMIIE9jCCAt6''' + with self.raises(s_exc.BadPkgDef) as exc: + await core.addStormPkg({'codesign': {'cert': cert, 'sign': 'bar'}}, verify=True) + self.eq(exc.exception.get('mesg'), 'Storm package has malformed certificate!') + + usercertpath = core.certdir.getUserCertPath('notCodeCert') + with s_common.genfile(usercertpath) as fd: + cert = fd.read().decode() + with self.raises(s_exc.BadCertBytes) as exc: + await core.addStormPkg({'codesign': {'cert': cert, 'sign': 'bar'}}, verify=True) + self.eq(exc.exception.get('mesg'), 'Certificate is not for code signing.') + + # revoke our code signing cert and attempt to load + crl = core.certdir.genCaCrl(immname) + crl.revoke(codecert) + + with self.raises(s_exc.BadPkgDef) as exc: + await core.addStormPkg(pkgorig, verify=True) + self.eq(exc.exception.get('mesg'), 'Storm package has invalid certificate: certificate revoked') From f8b96c806e0b135c8a481972987269bfebc08925 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 14 Feb 2024 21:52:02 +0000 Subject: [PATCH 20/31] Remove old type hints from documentation, fix example rendering --- synapse/lib/certdir.py | 269 +++++++++++++++--------------- synapse/tests/test_lib_certdir.py | 43 ++++- 2 files changed, 178 insertions(+), 134 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 5bc4dd6b57..9ffbf7cd5a 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -126,7 +126,7 @@ def revoke(self, cert: c_x509.Certificate) -> None: Revoke a certificate with the CRL. Args: - cert (cryto.X509): The certificate to revoke. + cert: The certificate to revoke. Returns: None @@ -213,7 +213,7 @@ class CertDir: * CertDir does not currently support signing CA CSRs. ''' - def __init__(self, path=None): + def __init__(self, path: StrOrNoneType =None): self.crypto_numbits = 4096 self.signing_digest = c_hashes.SHA256 @@ -229,7 +229,7 @@ def __init__(self, path=None): for p in path: self.addCertPath(p) - def addCertPath(self, *path): + def addCertPath(self, *path: str): fullpath = s_common.genpath(*path) self.pathrefs[fullpath] += 1 @@ -237,7 +237,7 @@ def addCertPath(self, *path): if self.pathrefs[fullpath] == 1: self.certdirs.append(fullpath) - def delCertPath(self, *path): + def delCertPath(self, *path: str): fullpath = s_common.genpath(*path) self.pathrefs[fullpath] -= 1 if self.pathrefs[fullpath] <= 0: @@ -245,26 +245,25 @@ def delCertPath(self, *path): self.pathrefs.pop(fullpath, None) def genCaCert(self, name: str, - signas: Optional[str | None] = None, + signas: StrOrNoneType = None, outp: OutPutOrNoneType =None, save: bool =True) -> PkeyAndCertType: ''' Generates a CA keypair. Args: - name (str): The name of the CA keypair. - signas (str): The CA keypair to sign the new CA with. - outp (synapse.lib.output.OutPut): The output buffer. - save (bool): + name: The name of the CA keypair. + signas: The CA keypair to sign the new CA with. + outp: The output buffer. + save: Safe the certificate and key to disk. Examples: - Make a CA named "myca": + Make a CA named "myca":: mycakey, mycacert = cdir.genCaCert('myca') Returns: - #XXX FIX bytes: Generates a host certificate signing request. Args: - name (str): The name of the host CSR. - outp (synapse.lib.output.Output): The output buffer. + name: The name of the host CSR. + outp: The output buffer. Examples: - Generate a CSR for the host key named "myhost": + Generate a CSR for the host key named "myhost":: cdir.genHostCsr('myhost') Returns: - bytes: The bytes of the CSR. + The bytes of the CSR. ''' return self._genPkeyCsr(name, 'hosts', outp=outp) @@ -407,18 +406,18 @@ def genUserCert(self, Generates a user keypair. Args: - name (str): The name of the user keypair. - signas (str): The CA keypair to sign the new user keypair with. - outp (synapse.lib.output.Output): The output buffer. - csr (OpenSSL.crypto.PKey): The CSR public key when generating the keypair from a CSR. + name: The name of the user keypair. + signas: The CA keypair to sign the new user keypair with. + outp: The output buffer. + csr: The CSR public key when generating the keypair from a CSR. Examples: - Generate a user cert for the user "myuser": + Generate a user cert for the user "myuser":: myuserkey, myusercert = cdir.genUserCert('myuser') Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. + Tuple containing the key and certificate objects. ''' if csr is None: prvkey = self._genPrivKey() @@ -465,18 +464,18 @@ def genCodeCert(self, name: str, signas: StrOrNoneType =None, outp: OutPutOrNone Generates a code signing keypair. Args: - name (str): The name of the code signing cert. - signas (str): The CA keypair to sign the new code keypair with. - outp (synapse.lib.output.Output): The output buffer. + name: The name of the code signing cert. + signas: The CA keypair to sign the new code keypair with. + outp: The output buffer. Examples: - Generate a code signing cert for the name "The Vertex Project": + Generate a code signing cert for the name "The Vertex Project":: myuserkey, myusercert = cdir.genCodeCert('The Vertex Project') Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the key and certificate objects. + Tuple containing the key and certificate objects. ''' prvkey = self._genPrivKey() pubkey = prvkey.public_key() @@ -545,10 +544,13 @@ def valCodeCert(self, byts: bytes) -> c_x509.Certificate: Verify a code cert is valid according to certdir's available CAs and CRLs. Args: - byts (bytes): The certificate bytes. + byts: The certificate bytes. + + Raises: + s_exc.BadCertVerify if we are unable to verify the certificate. Returns: - OpenSSL.crypto.X509: The certificate. + The certificate. ''' reqext = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]) @@ -600,6 +602,7 @@ def _getCaCrls(self) -> List[c_x509.CertificateRevocationList]: def genClientCert(self, name: str, outp: OutPutOrNoneType =None) -> None: ''' Generates a user PKCS #12 archive. + Please note that the resulting file will contain private key material. Args: @@ -607,7 +610,7 @@ def genClientCert(self, name: str, outp: OutPutOrNoneType =None) -> None: outp (synapse.lib.output.Output): The output buffer. Examples: - Make the PKC12 object for user "myuser": + Make the PKC12 object for user "myuser":: myuserpkcs12 = cdir.genClientCert('myuser') @@ -636,26 +639,25 @@ def genClientCert(self, name: str, outp: OutPutOrNoneType =None) -> None: if outp is not None: outp.printf('client cert saved: %s' % (crtpath,)) - def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | None] =None): + def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | None] =None) -> c_x509.Certificate: ''' Validate the PEM encoded x509 user certificate bytes and return it. Args: - byts (bytes): The bytes for the User Certificate. - cacerts (tuple): A tuple of OpenSSL.crypto.X509 CA Certificates. + byts: The bytes for the User Certificate. + cacerts: A tuple of CA Certificates to use for validating the user cert.. Raises: BadCertVerify: If the certificate is not valid. Returns: - OpenSSL.crypto.X509: The certificate, if it is valid. + The certificate, if it is valid. ''' reqext = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]) cert = self.loadCertByts(byts) eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) - # XXX FIXME if eku is none??/ if reqext != eku.value: mesg = 'Certificate is not for client auth.' raise s_exc.BadCertBytes(mesg=mesg) @@ -679,34 +681,34 @@ def genUserCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: Generates a user certificate signing request. Args: - name (str): The name of the user CSR. - outp (synapse.lib.output.Output): The output buffer. + name: The name of the user CSR. + outp: The output buffer. Examples: - Generate a CSR for the user "myuser": + Generate a CSR for the user "myuser":: cdir.genUserCsr('myuser') Returns: - bytes: The bytes of the CSR. + The bytes of the CSR. ''' return self._genPkeyCsr(name, 'users', outp=outp) - def getCaCert(self, name): + def getCaCert(self, name: str) -> CertOrNoneType: ''' Loads the X509 object for a given CA. Args: - name (str): The name of the CA keypair. + name: The name of the CA keypair. Examples: - Get the certificate for the CA "myca" + + Get the certificate for the CA "myca":: mycacert = cdir.getCaCert('myca') Returns: - # FIXME - CORRECT TYPE - : The certificate, if exists. + The certificate, if exists. ''' return self._loadCertPath(self.getCaCertPath(name)) @@ -721,7 +723,7 @@ def getCaCerts(self) -> List[c_x509.Certificate]: Return a list of CA certs from the CertDir. Returns: - [OpenSSL.crypto.X509]: List of CA certificates. + List of CA certificates. ''' retn = [] @@ -746,15 +748,15 @@ def getCaCertPath(self, name: str) -> StrOrNoneType: Gets the path to a CA certificate. Args: - name (str): The name of the CA keypair. + name: The name of the CA keypair. Examples: - Get the path to the CA certificate for the CA "myca": + Get the path to the CA certificate for the CA "myca":: mypath = cdir.getCACertPath('myca') Returns: - str: The path if exists. + The path if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'cas', '%s.crt' % name) @@ -766,16 +768,15 @@ def getCaKey(self, name) -> PkeyOrNoneType: Loads the PKey object for a given CA keypair. Args: - name (str): The name of the CA keypair. + name: The name of the CA keypair. Examples: - Get the private key for the CA "myca": + Get the private key for the CA "myca":: mycakey = cdir.getCaKey('myca') Returns: - # XXX FIXME Correct the type - : The private key, if exists. + The private key, if exists. ''' return self._loadKeyPath(self.getCaKeyPath(name)) @@ -784,7 +785,7 @@ def getCaKeyPath(self, name: str) -> StrOrNoneType: Gets the path to a CA key. Args: - name (str): The name of the CA keypair. + name: The name of the CA keypair. Examples: Get the path to the private key for the CA "myca": @@ -792,7 +793,7 @@ def getCaKeyPath(self, name: str) -> StrOrNoneType: mypath = cdir.getCAKeyPath('myca') Returns: - str: The path if exists. + The path if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'cas', '%s.key' % name) @@ -804,7 +805,7 @@ def getClientCert(self, name: str) -> Pkcs12OrNoneType: Loads the PKCS12 archive object for a given user keypair. Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: Get the PKCS12 object for the user "myuser": @@ -815,7 +816,7 @@ def getClientCert(self, name: str) -> Pkcs12OrNoneType: The PKCS12 archive will contain private key material if it was created with CertDir or the easycert tool Returns: - OpenSSL.crypto.PKCS12: The PKCS12 archive, if exists. + The PKCS12 archive, if exists. ''' return self._loadP12Path(self.getClientCertPath(name)) @@ -824,7 +825,7 @@ def getClientCertPath(self, name: str) -> StrOrNoneType: Gets the path to a client certificate. Args: - name (str): The name of the client keypair. + name: The name of the client keypair. Examples: Get the path to the client certificate for "myuser": @@ -832,7 +833,7 @@ def getClientCertPath(self, name: str) -> StrOrNoneType: mypath = cdir.getClientCertPath('myuser') Returns: - str: The path if exists. + The path if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.p12' % name) @@ -844,15 +845,16 @@ def getHostCaPath(self, name: str) -> StrOrNoneType: Gets the path to the CA certificate that issued a given host keypair. Args: - name (str): The name of the host keypair. + name: The name of the host keypair. Examples: - Get the path to the CA cert which issue the cert for "myhost": + + Get the path to the CA cert which issue the cert for "myhost":: mypath = cdir.getHostCaPath('myhost') Returns: - str: The path if exists. + The path if exists. ''' cert = self.getHostCert(name) if cert is None: @@ -865,15 +867,15 @@ def getHostCert(self, name: str) -> CertOrNoneType: Loads the X509 object for a given host keypair. Args: - name (str): The name of the host keypair. + name: The name of the host keypair. Examples: - Get the certificate object for the host "myhost": + Get the certificate object for the host "myhost":: myhostcert = cdir.getHostCert('myhost') Returns: - OpenSSL.crypto.X509: The certificate, if exists. + The certificate, if exists. ''' return self._loadCertPath(self.getHostCertPath(name)) @@ -888,7 +890,7 @@ def getHostCertPath(self, name: str) -> StrOrNoneType: Gets the path to a host certificate. Args: - name (str): The name of the host keypair. + name: The name of the host keypair. Examples: Get the path to the host certificate for the host "myhost": @@ -896,7 +898,7 @@ def getHostCertPath(self, name: str) -> StrOrNoneType: mypath = cdir.getHostCertPath('myhost') Returns: - str: The path if exists. + The path if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'hosts', '%s.crt' % name) @@ -908,15 +910,15 @@ def getHostKey(self, name: str) -> PkeyOrNoneType: Loads the PKey object for a given host keypair. Args: - name (str): The name of the host keypair. + name: The name of the host keypair. Examples: - Get the private key object for the host "myhost": + Get the private key object for the host "myhost":: myhostkey = cdir.getHostKey('myhost') Returns: - OpenSSL.crypto.PKey: The private key, if exists. + The private key, if exists. ''' return self._loadKeyPath(self.getHostKeyPath(name)) @@ -925,10 +927,10 @@ def getHostKeyPath(self, name: str) -> StrOrNoneType: Gets the path to a host key. Args: - name (str): The name of the host keypair. + name: The name of the host keypair. Examples: - Get the path to the host key for the host "myhost": + Get the path to the host key for the host "myhost":: mypath = cdir.getHostKeyPath('myhost') @@ -945,15 +947,15 @@ def getUserCaPath(self, name: str) -> StrOrNoneType: Gets the path to the CA certificate that issued a given user keypair. Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: - Get the path to the CA cert which issue the cert for "myuser": + Get the path to the CA cert which issue the cert for "myuser":: mypath = cdir.getUserCaPath('myuser') Returns: - str: The path if exists. + The path if exists. ''' cert = self.getUserCert(name) if cert is None: @@ -966,15 +968,15 @@ def getUserCert(self, name: str) -> CertOrNoneType: Loads the X509 object for a given user keypair. Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: - Get the certificate object for the user "myuser": + Get the certificate object for the user "myuser":: myusercert = cdir.getUserCert('myuser') Returns: - OpenSSL.crypto.X509: The certificate, if exists. + The certificate, if exists. ''' return self._loadCertPath(self.getUserCertPath(name)) @@ -986,12 +988,12 @@ def getUserCertPath(self, name: str) -> StrOrNoneType: name (str): The name of the user keypair. Examples: - Get the path for the user cert for "myuser": + Get the path for the user cert for "myuser":: mypath = cdir.getUserCertPath('myuser') Returns: - str: The path if exists. + The path if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.crt' % name) @@ -1003,11 +1005,11 @@ def getUserForHost(self, user: str, host: str) -> StrOrNoneType: Gets the name of the first existing user cert for a given user and host. Args: - user (str): The name of the user. - host (str): The name of the host. + user: The name of the user. + host: The name of the host. Examples: - Get the name for the "myuser" user cert at "cool.vertex.link": + Get the name for the "myuser" user cert at "cool.vertex.link":: usercertname = cdir.getUserForHost('myuser', 'cool.vertex.link') @@ -1025,15 +1027,15 @@ def getUserKey(self, name: str) -> PkeyOrNoneType: Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: - Get the key object for the user key for "myuser": + Get the key object for the user key for "myuser":: myuserkey = cdir.getUserKey('myuser') Returns: - OpenSSL.crypto.PKey: The private key, if exists. + The private key, if exists. ''' return self._loadKeyPath(self.getUserKeyPath(name)) @@ -1042,15 +1044,15 @@ def getUserKeyPath(self, name: str) -> StrOrNoneType: Gets the path to a user key. Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: - Get the path to the user key for "myuser": + Get the path to the user key for "myuser":: mypath = cdir.getUserKeyPath('myuser') Returns: - str: The path if exists. + The path if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.key' % name) @@ -1073,11 +1075,11 @@ def importFile(self, path: str, mode: str, outp: OutPutOrNoneType =None) -> None Imports certs and keys into the Synapse cert directory Args: - path (str): The path of the file to be imported. - mode (str): The certdir subdirectory to import the file into. + path: The path of the file to be imported. + mode: The certdir subdirectory to import the file into. Examples: - Import CA certifciate 'mycoolca.crt' to the 'cas' directory. + Import CA certifciate 'mycoolca.crt' to the 'cas' directory:: certdir.importFile('mycoolca.crt', 'cas') @@ -1113,15 +1115,15 @@ def isCaCert(self, name: str) -> bool: Checks if a CA certificate exists. Args: - name (str): The name of the CA keypair. + name: The name of the CA keypair. Examples: - Check if the CA certificate for "myca" exists: + Check if the CA certificate for "myca" exists:: exists = cdir.isCaCert('myca') Returns: - bool: True if the certificate is present, False otherwise. + True if the certificate is present, False otherwise. ''' return self.getCaCertPath(name) is not None @@ -1130,15 +1132,15 @@ def isClientCert(self, name: str) -> bool: Checks if a user client certificate (PKCS12) exists. Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: - Check if the client certificate "myuser" exists: + Check if the client certificate "myuser" exists:: exists = cdir.isClientCert('myuser') Returns: - bool: True if the certificate is present, False otherwise. + True if the certificate is present, False otherwise. ''' crtpath = self._getPathJoin('users', '%s.p12' % name) return os.path.isfile(crtpath) @@ -1148,15 +1150,15 @@ def isHostCert(self, name: str) -> bool: Checks if a host certificate exists. Args: - name (str): The name of the host keypair. + name: The name of the host keypair. Examples: - Check if the host cert "myhost" exists: + Check if the host cert "myhost" exists:: exists = cdir.isUserCert('myhost') Returns: - bool: True if the certificate is present, False otherwise. + True if the certificate is present, False otherwise. ''' return self.getHostCertPath(name) is not None @@ -1165,15 +1167,15 @@ def isUserCert(self, name: str) -> bool: Checks if a user certificate exists. Args: - name (str): The name of the user keypair. + name: The name of the user keypair. Examples: - Check if the user cert "myuser" exists: + Check if the user cert "myuser" exists:: exists = cdir.isUserCert('myuser') Returns: - bool: True if the certificate is present, False otherwise. + True if the certificate is present, False otherwise. ''' return self.getUserCertPath(name) is not None @@ -1182,11 +1184,11 @@ def signCertAs(self, builder: c_x509.CertificateBuilder, signas: str) -> c_x509. Signs a certificate with a CA keypair. Args: - cert (OpenSSL.crypto.X509): The certificate to sign. - signas (str): The CA keypair name to sign the new keypair with. + cert: The certificate to sign. + signas: The CA keypair name to sign the new keypair with. Examples: - Sign a certificate with the CA "myca": + Sign a certificate with the CA "myca":: cdir.signCertAs(mycert, 'myca') @@ -1220,18 +1222,18 @@ def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, Signs a host CSR with a CA keypair. Args: - xcsr (OpenSSL.crypto.X509Req): The certificate signing request. - signas (str): The CA keypair name to sign the CSR with. - outp (synapse.lib.output.Output): The output buffer. - sans (list): List of subject alternative names. + xcsr: The certificate signing request. + signas: The CA keypair name to sign the CSR with. + outp: The output buffer. + sans: List of subject alternative names. Examples: - Sign a host key with the CA "myca": + Sign a host key with the CA "myca":: cdir.signHostCsr(mycsr, 'myca') Returns: - ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. + Tuple containing the public key and certificate objects. ''' pkey = xcsr.public_key() attr = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] @@ -1243,11 +1245,11 @@ def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_ Self-sign a certificate. Args: - cert (OpenSSL.crypto.X509): The certificate to sign. - pkey (OpenSSL.crypto.PKey): The PKey with which to sign the certificate. + cert: The certificate to sign. + pkey: The PKey with which to sign the certificate. Examples: - Sign a given certificate with a given private key: + Sign a given certificate with a given private key:: cdir.selfSignCert(mycert, myotherprivatekey) @@ -1272,12 +1274,15 @@ def signUserCsr(self, xcsr: c_x509.CertificateSigningRequest, Signs a user CSR with a CA keypair. Args: - xcsr (OpenSSL.crypto.X509Req): The certificate signing request. - signas (str): The CA keypair name to sign the CSR with. - outp (synapse.lib.output.Output): The output buffer. + xcsr: The certificate signing request. + signas: The CA keypair name to sign the CSR with. + outp: The output buffer. Examples: - cdir.signUserCsr(mycsr, 'myca') + + Sign a user CSR with "myca":: + + cdir.signUserCsr(mycsr, 'myca') Returns: ((OpenSSL.crypto.PKey, OpenSSL.crypto.X509)): Tuple containing the public key and certificate objects. @@ -1307,7 +1312,7 @@ def getClientSSLContext(self, certname: StrOrNoneType =None) -> ssl.SSLContext: certname: If specified, use the user certificate with the matching name to authenticate to the remote service. Returns: - ssl.SSLContext: A SSLContext object. + A SSLContext object. ''' sslctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) sslctx.minimum_version = ssl.TLSVersion.TLSv1_2 @@ -1351,7 +1356,7 @@ def getServerSSLContext(self, hostname: StrOrNoneType =None, caname: StrOrNoneTy caname: If not None, the given name is used to locate a CA certificate used to validate client SSL certs. Returns: - ssl.SSLContext: A SSLContext object. + A SSLContext object. ''' if hostname is not None and hostname.find(',') != -1: # multi-hostname SNI routing has been requested @@ -1388,10 +1393,10 @@ def genCaCrl(self, name: str) -> Crl: Get the CRL for a given CA. Args: - name (str): The CA name. + name: The CA name. Returns: - CRL: The CRL object. + The CRL object. ''' return Crl(self, name) @@ -1532,10 +1537,10 @@ def loadCertByts(self, byts: bytes) -> c_x509.Certificate: Load a X509 certificate from its PEM encoded bytes. Args: - byts (bytes): The PEM encoded bytes of the certificate. + byts: The PEM encoded bytes of the certificate. Returns: - OpenSSL.crypto.X509: The X509 certificate. + The X509 certificate. Raises: BadCertBytes: If the certificate bytes are invalid. diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 94f33f91d6..d235d09af0 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -280,6 +280,42 @@ def p12_assertions(self, encryption_algorithm=c_serialization.NoEncryption()) self.eq(_cb, _pb) + def code_assertions(self, + cdir: s_certdir.CertDir, + cert: c_x509.Certificate, + key: s_certdir.PkeyType, + cacert: c_x509.Certificate = None + ): + reqbc = c_x509.BasicConstraints(ca=False, path_length=None) + reqeku = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]) + reqku = c_x509.KeyUsage(digital_signature=True, content_commitment=False, key_encipherment=False, + data_encipherment=False, key_agreement=False, key_cert_sign=False, + crl_sign=False, encipher_only=False, decipher_only=False) + reqnstype = c_x509.UnrecognizedExtension(c_x509.ObjectIdentifier(s_certdir.NSCERTTYPE_OID), + value=s_certdir.NSCERTTYPE_OBJSIGN) + + bc = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS) + self.eq(reqbc, bc.value) + + ku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.KEY_USAGE) + self.eq(reqku, ku.value) + + eku = cert.extensions.get_extension_for_oid(c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE) + self.eq(reqeku, eku.value) + + nstype = cert.extensions.get_extension_for_oid(c_x509.ObjectIdentifier(s_certdir.NSCERTTYPE_OID)) + self.eq(reqnstype, nstype.value) + + expected_oids = sorted([ + c_x509.oid.ExtensionOID.BASIC_CONSTRAINTS.dotted_string, + c_x509.oid.ExtensionOID.KEY_USAGE.dotted_string, + c_x509.oid.ExtensionOID.EXTENDED_KEY_USAGE.dotted_string, + s_certdir.NSCERTTYPE_OID, + ]) + + ext_oids = sorted([ext.oid.dotted_string for ext in cert.extensions]) + self.eq(expected_oids, ext_oids) + def test_certdir_cas(self): with self.getCertDir() as cdir: # type: s_certdir.CertDiNew @@ -672,15 +708,18 @@ async def test_certdir_codesign(self): codename3 = 'Vertex Build Pipeline Triple Threat' cdir.genCaCert(caname) - cdir.genCaCert(immname, signas=caname) + _, cacert = cdir.genCaCert(immname, signas=caname) cdir.genUserCert('notCodeCert', signas=caname, ) cdir.genCaCrl(caname)._save() cdir.genCaCrl(immname)._save() - cdir.genCodeCert(codename, signas=immname) + pkey, cert = cdir.genCodeCert(codename, signas=immname) + self.code_assertions(cdir, cert, pkey, cacert) + rsak = cdir.getCodeKey(codename) cert = cdir.getCodeCert(codename) + self.isinstance(cert, c_x509.Certificate) rsap = rsak.public() From 41bf934b86c3d38333fabfa4e5c3a3753f0c7211 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 14 Feb 2024 23:21:50 +0000 Subject: [PATCH 21/31] Remove fixme; remove bad type hints from tests --- synapse/tests/test_lib_certdir.py | 43 +++++++++++++------------------ 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index d235d09af0..1b059cc061 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -50,10 +50,10 @@ def basic_assertions(self, test basic certificate assumptions Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - cacert (crypto.X509): Corresponding CA cert (optional) + cdir: certdir object + cert: Cert to test + key: Key for the certification + cacert: Corresponding CA cert (optional) ''' self.nn(cert) self.nn(key) @@ -95,14 +95,7 @@ def basic_assertions(self, salt_length=c_padding.PSS.MAX_LENGTH), algorithm=c_hashes.SHA256(), ) - # XXX FIXME - Figure out a parallel for this in cryptography parlance? - # This is demonstrative of a a high level of control over a SSL Context that - # we don't actually utilize. ??? - # # ensure that a ssl context using both cert/key match - # sslcontext = SSL.Context(SSL.TLSv1_2_METHOD) - # sslcontext.use_certificate(cert) - # sslcontext.use_privatekey(key) - # self.none(sslcontext.check_privatekey()) + self.eq(key.public_key(), pubkey) if cacert: @@ -153,10 +146,10 @@ def host_assertions(self, test basic certificate assumptions for a host certificate Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - cacert (crypto.X509): Corresponding CA cert (optional) + cdir: certdir object + cert: Cert to test + key: Key for the certification + cacert: Corresponding CA cert (optional) ''' reqbc = c_x509.BasicConstraints(ca=False, path_length=None) @@ -199,10 +192,10 @@ def user_assertions(self, test basic certificate assumptions for a user certificate Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - cacert (crypto.X509): Corresponding CA cert (optional) + cdir: certdir object + cert: Cert to test + key: Key for the certification + cacert: Corresponding CA cert (optional) ''' reqbc = c_x509.BasicConstraints(ca=False, path_length=None) reqeku = c_x509.ExtendedKeyUsage([c_x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH]) @@ -244,11 +237,11 @@ def p12_assertions(self, test basic p12 certificate bundle assumptions Args: - cdir (s_certdir.CertDir): certdir object - cert (crypto.X509): Cert to test - key (crypto.PKey): Key for the certification - p12 (crypto.PKCS12): PKCS12 object to test - cacert (crypto.X509): Corresponding CA cert (optional) + cdir: certdir object + cert: Cert to test + key: Key for the certification + p12: PKCS12 object to test + cacert: Corresponding CA cert (optional) ''' self.nn(p12) From b72aa95ff40a47bb9b0965435fdb253db22989c1 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 15 Feb 2024 14:11:56 +0000 Subject: [PATCH 22/31] Tidy up easycert test coverage --- synapse/lib/certdir.py | 28 +++++++-- synapse/tests/test_lib_certdir.py | 85 ++++++++++++++++++++++++---- synapse/tests/test_tools_easycert.py | 29 ++++++++++ 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 9ffbf7cd5a..f19cb914eb 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -1179,6 +1179,23 @@ def isUserCert(self, name: str) -> bool: ''' return self.getUserCertPath(name) is not None + def isCodeCert(self, name: str) -> bool: + ''' + Checks if a code certificate exists. + + Args: + name: The name of the code keypair. + + Examples: + Check if the code cert "mypipeline" exists:: + + exists = cdir.isCodeCert('mypipeline') + + Returns: + True if the certificate is present, False otherwise. + ''' + return self.getCodeCert(name) is not None + def signCertAs(self, builder: c_x509.CertificateBuilder, signas: str) -> c_x509.Certificate: ''' Signs a certificate with a CA keypair. @@ -1428,8 +1445,6 @@ def _getServerSSLContext(self, hostname=None, caname=None) -> ssl.SSLContext: return sslctx - # XXX FIXME - Add test_lib_certdir tests for these save APIS - def saveCertPem(self, cert: c_x509.Certificate, path: str) -> None: ''' Save a certificate in PEM format to a file outside the certdir. @@ -1468,6 +1483,12 @@ def saveUserCertByts(self, byts: bytes) -> str: name = attr.value return self._saveCertTo(cert, 'users', f'{name}.crt') + def saveCodeCertBytes(self, byts: bytes) -> str: + cert = self._loadCertByts(byts) + attr = cert.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] + name = attr.value + return self._saveCertTo(cert, 'code', f'{name}.crt') + def _checkDupFile(self, path) -> None: if os.path.isfile(path): raise s_exc.DupFileName(mesg=f'Duplicate file {path}', path=path) @@ -1567,8 +1588,7 @@ def _loadKeyPath(self, path: str) -> PkeyOrNoneType: pkey = c_serialization.load_pem_private_key(byts, password=None) if isinstance(pkey, (c_rsa.RSAPrivateKey, c_dsa.DSAPrivateKey)): return pkey - # XXX FIXME Coverage for this! - raise s_exc.BadCertBytes(mesg=f'Key at {path} is {type(pkey)}, expected a DSA or RSA key.', + raise s_exc.BadCertBytes(mesg=f'Key is {pkey.__class__.__name__}, expected a DSA or RSA key, {path=}', path=path) def _loadP12Path(self, path: str) -> Pkcs12OrNoneType: diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 1b059cc061..3c30c05eda 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -1,27 +1,21 @@ import os import ssl -import datetime -from contextlib import contextmanager +import contextlib -from OpenSSL import crypto, SSL +from OpenSSL import crypto import synapse.exc as s_exc import synapse.common as s_common -import synapse.lib.output as s_output import synapse.lib.certdir as s_certdir import synapse.lib.msgpack as s_msgpack import synapse.tests.utils as s_t_utils import synapse.tools.genpkg as s_genpkg -import synapse.tools.easycert as s_easycert - -import synapse.lib.crypto.rsa as s_crypto_rsa import cryptography.x509 as c_x509 import cryptography.exceptions as c_exc -import cryptography.x509.verification as c_verification import cryptography.hazmat.primitives.hashes as c_hashes +import cryptography.hazmat.primitives.asymmetric.ec as c_ec import cryptography.hazmat.primitives.asymmetric.rsa as c_rsa -import cryptography.hazmat.primitives.asymmetric.dsa as c_dsa import cryptography.hazmat.primitives.asymmetric.padding as c_padding import cryptography.hazmat.primitives.serialization as c_serialization import cryptography.hazmat.primitives.serialization.pkcs12 as c_pkcs12 @@ -29,13 +23,13 @@ class CertDirTest(s_t_utils.SynTest): - @contextmanager + @contextlib.contextmanager def getCertDir(self): ''' Get a test CertDir object. Yields: - s_certdir.CertDir: A certdir object based out of a temp directory. + A certdir object based out of a temp directory. ''' # create a temp folder and make it a cert dir with self.getTestDir() as dirname: @@ -862,3 +856,72 @@ async def test_cortex_codesign(self): with self.raises(s_exc.BadPkgDef) as exc: await core.addStormPkg(pkgorig, verify=True) self.eq(exc.exception.get('mesg'), 'Storm package has invalid certificate: certificate revoked') + + def test_certdir_save_load(self): + + with self.getCertDir() as cdir: # type: s_certdir.CertDir + caname = 'TestCA' + hostname = 'wee.wow.com' + username = 'dude@wow.com' + codename = 'wow pipe' + + pkey, cert = cdir.genCaCert(caname) + cdir.genHostCert(hostname, signas=caname) + cdir.genUserCert(username, signas=caname) + cdir.genCodeCert(codename, signas=caname) + + with self.getTestDir() as dirn: + cert_path = s_common.genpath(dirn, 'ca.crt') + cdir.saveCertPem(cert, cert_path) + with s_common.genfile(cert_path) as fd: + cert_copy = fd.read() + + key_path = s_common.genpath(dirn, 'ca.key') + cdir.savePkeyPem(pkey, key_path) + with s_common.genfile(key_path) as fd: + pkey_copy = fd.read() + with s_common.genfile(cdir.getCaKeyPath(caname)) as fd: + cdir_pkey_bytes = fd.read() + + self.eq(cert_copy, cdir.getCaCertBytes(caname)) + self.eq(pkey_copy, cdir_pkey_bytes) + + ca_path = cdir.getCaCertPath(caname) + h_path = cdir.getHostCertPath(hostname) + u_path = cdir.getUserCertPath(username) + co_path = cdir.getCodeCertPath(codename) + + with self.getCertDir() as cdir2: # type: s_certdir.CertDir + + with s_common.genfile(ca_path) as fd: + byts = fd.read() + cdir2.saveCaCertByts(byts) + + with s_common.genfile(h_path) as fd: + byts = fd.read() + cdir2.saveHostCertByts(byts) + + with s_common.genfile(u_path) as fd: + byts = fd.read() + cdir2.saveUserCertByts(byts) + + with s_common.genfile(co_path) as fd: + byts = fd.read() + cdir2.saveCodeCertBytes(byts) + + self.true(cdir2.isCaCert(caname)) + self.true(cdir2.isHostCert(hostname)) + self.true(cdir2.isUserCert(username)) + self.true(cdir2.isCodeCert(codename)) + + # older PyOpenSSL code assumed loading a pkey was always a DSA or RSA key. + pkey = c_ec.generate_private_key(c_ec.SECP384R1()) + byts = pkey.private_bytes(encoding=c_serialization.Encoding.PEM, + format=c_serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=c_serialization.NoEncryption(), ) + path = cdir._getPathJoin('newp', 'ec.key') + with s_common.genfile(path) as fd: + fd.write(byts) + with self.raises(s_exc.BadCertBytes) as cm: + cdir._loadKeyPath(path) + self.isin('Key is ECPrivateKey, expected a DSA or RSA key', cm.exception.get('mesg')) diff --git a/synapse/tests/test_tools_easycert.py b/synapse/tests/test_tools_easycert.py index 8df634914e..da25250fb1 100644 --- a/synapse/tests/test_tools_easycert.py +++ b/synapse/tests/test_tools_easycert.py @@ -151,15 +151,38 @@ def test_easycert_importfile(self): argv = ['--importfile', 'cas', '--certdir', tstpath, 'nope'] self.raises(s_exc.NoSuchFile, s_easycert.main, argv, outp=outp) + def test_easycert_code(self): + with self.getTestDir() as dirn: + outp = self.getTestOutp() + self.eq(0, s_easycert.main(('--certdir', dirn, '--ca', 'woot'), outp=outp)) + + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--code', 'wootpipe', '--signas', 'woot'), outp=outp)) + outp.expect('code/wootpipe.crt') + + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--code', 'selfpipe'), outp=outp)) + outp.expect('code/selfpipe.crt') + def test_easycert_revokeas(self): with self.getTestDir() as dirn: outp = self.getTestOutp() self.eq(0, s_easycert.main(('--certdir', dirn, '--ca', 'woot'), outp=outp)) + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--ca', 'inner', '--signas', 'woot'), outp=outp)) + + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--crl', 'woot'), outp=outp)) + outp.expect('CRL saved:') + outp.clear() self.eq(0, s_easycert.main(('--certdir', dirn, '--signas', 'woot', 'newp@newp.newp'), outp=outp)) + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--signas', 'woot', '--code', 'testpipe'), outp=outp)) + outp.clear() self.eq(0, s_easycert.main(('--certdir', dirn, '--signas', 'woot', '--server', 'newp.newp'), outp=outp)) @@ -169,6 +192,12 @@ def test_easycert_revokeas(self): outp.clear() self.eq(0, s_easycert.main(('--certdir', dirn, '--revokeas', 'woot', '--server', 'newp.newp'), outp=outp)) + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--revokeas', 'woot', '--ca', 'inner'), outp=outp)) + + outp.clear() + self.eq(0, s_easycert.main(('--certdir', dirn, '--revokeas', 'woot', '--code', 'testpipe'), outp=outp)) + outp.clear() self.eq(1, s_easycert.main(('--certdir', dirn, '--revokeas', 'woot', 'noexist'), outp=outp)) outp.expect('Certificate not found: noexist') From cce421a1823346861d8ce2751c79700482974257 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 15 Feb 2024 14:16:06 +0000 Subject: [PATCH 23/31] Shorten up type hinting labels --- synapse/lib/certdir.py | 134 +++++++++++++++--------------- synapse/tests/test_lib_certdir.py | 34 ++++---- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index f19cb914eb..abbde5a938 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -40,17 +40,17 @@ TEN_YEARS = 10 * 365 * 24 * 60 * 60 # 10 years in seconds TEN_YEARS_TD = datetime.timedelta(seconds=TEN_YEARS) -StrOrNoneType = Union[str | None] -BytesOrNoneType = Union[bytes | None] -OutPutOrNoneType = Union[s_output.OutPut | None] -CertOrNoneType = Union[c_x509.Certificate | None] -PkeyOrNoneType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey | None] -PkeyAndCertType = Tuple[c_rsa.RSAPrivateKey, c_x509.Certificate] -PkeyAndBuilderType = Tuple[c_rsa.RSAPrivateKey, c_x509.CertificateBuilder] -PrivKeyPubKeyBuilderType = Tuple[Union[c_rsa.RSAPrivateKey | None], c_types.PublicKeyTypes, c_x509.CertificateBuilder] -PkeyType = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey] -Pkcs12OrNoneType = Union[c_pkcs12.PKCS12KeyAndCertificates | None] -PkeyOrNoneAndCertType = Tuple[Union[c_rsa.RSAPrivateKey | None], c_x509.Certificate] +StrOrNone = Union[str | None] +BytesOrNone = Union[bytes | None] +OutPutOrNone = Union[s_output.OutPut | None] +CertOrNone = Union[c_x509.Certificate | None] +PkeyOrNone = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey | None] +PkeyAndCert = Tuple[c_rsa.RSAPrivateKey, c_x509.Certificate] +PkeyAndBuilder = Tuple[c_rsa.RSAPrivateKey, c_x509.CertificateBuilder] +PrivKeyPubKeyBuilder = Tuple[Union[c_rsa.RSAPrivateKey | None], c_types.PublicKeyTypes, c_x509.CertificateBuilder] +Pkey = Union[c_rsa.RSAPrivateKey | c_dsa.DSAPrivateKey] +Pkcs12OrNone = Union[c_pkcs12.PKCS12KeyAndCertificates | None] +PkeyOrNoneAndCert = Tuple[Union[c_rsa.RSAPrivateKey | None], c_x509.Certificate] # Used for handling CSRs PubKeyOrNone = Union[c_types.PublicKeyTypes | None] @@ -213,7 +213,7 @@ class CertDir: * CertDir does not currently support signing CA CSRs. ''' - def __init__(self, path: StrOrNoneType =None): + def __init__(self, path: StrOrNone =None): self.crypto_numbits = 4096 self.signing_digest = c_hashes.SHA256 @@ -245,9 +245,9 @@ def delCertPath(self, *path: str): self.pathrefs.pop(fullpath, None) def genCaCert(self, name: str, - signas: StrOrNoneType = None, - outp: OutPutOrNoneType =None, - save: bool =True) -> PkeyAndCertType: + signas: StrOrNone = None, + outp: OutPutOrNone =None, + save: bool =True) -> PkeyAndCert: ''' Generates a CA keypair. @@ -289,11 +289,11 @@ def genCaCert(self, name: str, return prvkey, cert def genHostCert(self, name: str, - signas: StrOrNoneType = None, - outp: OutPutOrNoneType = None, + signas: StrOrNone = None, + outp: OutPutOrNone = None, csr: PubKeyOrNone =None, - sans: StrOrNoneType =None, - save: bool = True) -> PkeyOrNoneAndCertType: + sans: StrOrNone =None, + save: bool = True) -> PkeyOrNoneAndCert: ''' Generates a host keypair. @@ -378,7 +378,7 @@ def genHostCert(self, name: str, return prvkey, cert - def genHostCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: + def genHostCsr(self, name: str, outp: OutPutOrNone =None) -> bytes: ''' Generates a host certificate signing request. @@ -399,9 +399,9 @@ def genHostCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: def genUserCert(self, name: str, signas: Optional[str | None] = None, - outp: OutPutOrNoneType = None, + outp: OutPutOrNone = None, csr: PubKeyOrNone =None, - save: bool = True) -> PkeyOrNoneAndCertType: + save: bool = True) -> PkeyOrNoneAndCert: ''' Generates a user keypair. @@ -458,8 +458,8 @@ def genUserCert(self, return prvkey, cert - def genCodeCert(self, name: str, signas: StrOrNoneType =None, outp: OutPutOrNoneType =None, save: bool=True)\ - -> PkeyAndCertType: + def genCodeCert(self, name: str, signas: StrOrNone =None, outp: OutPutOrNone =None, save: bool=True)\ + -> PkeyAndCert: ''' Generates a code signing keypair. @@ -511,13 +511,13 @@ def genCodeCert(self, name: str, signas: StrOrNoneType =None, outp: OutPutOrNone return prvkey, cert - def getCodeKeyPath(self, name: str) -> StrOrNoneType: + def getCodeKeyPath(self, name: str) -> StrOrNone: for cdir in self.certdirs: path = s_common.genpath(cdir, 'code', f'{name}.key') if os.path.isfile(path): return path - def getCodeCertPath(self, name: str) -> StrOrNoneType: + def getCodeCertPath(self, name: str) -> StrOrNone: for cdir in self.certdirs: path = s_common.genpath(cdir, 'code', f'{name}.crt') if os.path.isfile(path): @@ -531,7 +531,7 @@ def getCodeKey(self, name: str) -> Union[s_rsa.PriKey | None]: pkey = self._loadKeyPath(path) return s_rsa.PriKey(pkey) - def getCodeCert(self, name: str) -> CertOrNoneType: + def getCodeCert(self, name: str) -> CertOrNone: path = self.getCodeCertPath(name) if path is None: # pragma: no cover @@ -599,7 +599,7 @@ def _getCaCrls(self) -> List[c_x509.CertificateRevocationList]: return crls - def genClientCert(self, name: str, outp: OutPutOrNoneType =None) -> None: + def genClientCert(self, name: str, outp: OutPutOrNone =None) -> None: ''' Generates a user PKCS #12 archive. @@ -676,7 +676,7 @@ def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | Non raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) return cert - def genUserCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: + def genUserCsr(self, name: str, outp: OutPutOrNone =None) -> bytes: ''' Generates a user certificate signing request. @@ -694,7 +694,7 @@ def genUserCsr(self, name: str, outp: OutPutOrNoneType =None) -> bytes: ''' return self._genPkeyCsr(name, 'users', outp=outp) - def getCaCert(self, name: str) -> CertOrNoneType: + def getCaCert(self, name: str) -> CertOrNone: ''' Loads the X509 object for a given CA. @@ -743,7 +743,7 @@ def getCaCerts(self) -> List[c_x509.Certificate]: return retn - def getCaCertPath(self, name: str) -> StrOrNoneType: + def getCaCertPath(self, name: str) -> StrOrNone: ''' Gets the path to a CA certificate. @@ -763,7 +763,7 @@ def getCaCertPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getCaKey(self, name) -> PkeyOrNoneType: + def getCaKey(self, name) -> PkeyOrNone: ''' Loads the PKey object for a given CA keypair. @@ -780,7 +780,7 @@ def getCaKey(self, name) -> PkeyOrNoneType: ''' return self._loadKeyPath(self.getCaKeyPath(name)) - def getCaKeyPath(self, name: str) -> StrOrNoneType: + def getCaKeyPath(self, name: str) -> StrOrNone: ''' Gets the path to a CA key. @@ -800,7 +800,7 @@ def getCaKeyPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getClientCert(self, name: str) -> Pkcs12OrNoneType: + def getClientCert(self, name: str) -> Pkcs12OrNone: ''' Loads the PKCS12 archive object for a given user keypair. @@ -820,7 +820,7 @@ def getClientCert(self, name: str) -> Pkcs12OrNoneType: ''' return self._loadP12Path(self.getClientCertPath(name)) - def getClientCertPath(self, name: str) -> StrOrNoneType: + def getClientCertPath(self, name: str) -> StrOrNone: ''' Gets the path to a client certificate. @@ -840,7 +840,7 @@ def getClientCertPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getHostCaPath(self, name: str) -> StrOrNoneType: + def getHostCaPath(self, name: str) -> StrOrNone: ''' Gets the path to the CA certificate that issued a given host keypair. @@ -862,7 +862,7 @@ def getHostCaPath(self, name: str) -> StrOrNoneType: return self._getCaPath(cert) - def getHostCert(self, name: str) -> CertOrNoneType: + def getHostCert(self, name: str) -> CertOrNone: ''' Loads the X509 object for a given host keypair. @@ -879,13 +879,13 @@ def getHostCert(self, name: str) -> CertOrNoneType: ''' return self._loadCertPath(self.getHostCertPath(name)) - def getHostCertHash(self, name: str) -> StrOrNoneType: + def getHostCertHash(self, name: str) -> StrOrNone: cert = self.getHostCert(name) if cert is None: return None return s_common.ehex(cert.fingerprint(c_hashes.SHA256())) - def getHostCertPath(self, name: str) -> StrOrNoneType: + def getHostCertPath(self, name: str) -> StrOrNone: ''' Gets the path to a host certificate. @@ -905,7 +905,7 @@ def getHostCertPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getHostKey(self, name: str) -> PkeyOrNoneType: + def getHostKey(self, name: str) -> PkeyOrNone: ''' Loads the PKey object for a given host keypair. @@ -922,7 +922,7 @@ def getHostKey(self, name: str) -> PkeyOrNoneType: ''' return self._loadKeyPath(self.getHostKeyPath(name)) - def getHostKeyPath(self, name: str) -> StrOrNoneType: + def getHostKeyPath(self, name: str) -> StrOrNone: ''' Gets the path to a host key. @@ -942,7 +942,7 @@ def getHostKeyPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getUserCaPath(self, name: str) -> StrOrNoneType: + def getUserCaPath(self, name: str) -> StrOrNone: ''' Gets the path to the CA certificate that issued a given user keypair. @@ -963,7 +963,7 @@ def getUserCaPath(self, name: str) -> StrOrNoneType: return self._getCaPath(cert) - def getUserCert(self, name: str) -> CertOrNoneType: + def getUserCert(self, name: str) -> CertOrNone: ''' Loads the X509 object for a given user keypair. @@ -980,7 +980,7 @@ def getUserCert(self, name: str) -> CertOrNoneType: ''' return self._loadCertPath(self.getUserCertPath(name)) - def getUserCertPath(self, name: str) -> StrOrNoneType: + def getUserCertPath(self, name: str) -> StrOrNone: ''' Gets the path to a user certificate. @@ -1000,7 +1000,7 @@ def getUserCertPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getUserForHost(self, user: str, host: str) -> StrOrNoneType: + def getUserForHost(self, user: str, host: str) -> StrOrNone: ''' Gets the name of the first existing user cert for a given user and host. @@ -1021,7 +1021,7 @@ def getUserForHost(self, user: str, host: str) -> StrOrNoneType: if self.isUserCert(usercert): return usercert - def getUserKey(self, name: str) -> PkeyOrNoneType: + def getUserKey(self, name: str) -> PkeyOrNone: ''' Loads the PKey object for a given user keypair. @@ -1039,7 +1039,7 @@ def getUserKey(self, name: str) -> PkeyOrNoneType: ''' return self._loadKeyPath(self.getUserKeyPath(name)) - def getUserKeyPath(self, name: str) -> StrOrNoneType: + def getUserKeyPath(self, name: str) -> StrOrNone: ''' Gets the path to a user key. @@ -1059,18 +1059,18 @@ def getUserKeyPath(self, name: str) -> StrOrNoneType: if os.path.isfile(path): return path - def getUserCsrPath(self, name: str) -> StrOrNoneType: + def getUserCsrPath(self, name: str) -> StrOrNone: for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.csr' % name) if os.path.isfile(path): return path - def getHostCsrPath(self, name: str) -> StrOrNoneType: + def getHostCsrPath(self, name: str) -> StrOrNone: for cdir in self.certdirs: path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) if os.path.isfile(path): return path - def importFile(self, path: str, mode: str, outp: OutPutOrNoneType =None) -> None: + def importFile(self, path: str, mode: str, outp: OutPutOrNone =None) -> None: ''' Imports certs and keys into the Synapse cert directory @@ -1232,9 +1232,9 @@ def signCertAs(self, builder: c_x509.CertificateBuilder, signas: str) -> c_x509. def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, signas: str, - outp: OutPutOrNoneType =None, - sans: StrOrNoneType =None, - save: bool =True) -> PkeyOrNoneAndCertType: + outp: OutPutOrNone =None, + sans: StrOrNone =None, + save: bool =True) -> PkeyOrNoneAndCert: ''' Signs a host CSR with a CA keypair. @@ -1257,7 +1257,7 @@ def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, name = attr.value return self.genHostCert(name, csr=pkey, signas=signas, outp=outp, sans=sans, save=save) - def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_x509.Certificate: + def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: Pkey) -> c_x509.Certificate: ''' Self-sign a certificate. @@ -1285,8 +1285,8 @@ def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: PkeyType) -> c_ def signUserCsr(self, xcsr: c_x509.CertificateSigningRequest, signas: str, - outp: OutPutOrNoneType=None, - save: bool =True) -> PkeyOrNoneAndCertType: + outp: OutPutOrNone=None, + save: bool =True) -> PkeyOrNoneAndCert: ''' Signs a user CSR with a CA keypair. @@ -1321,7 +1321,7 @@ def _loadCasIntoSSLContext(self, ctx): if name.endswith('.crt'): ctx.load_verify_locations(os.path.join(path, name)) - def getClientSSLContext(self, certname: StrOrNoneType =None) -> ssl.SSLContext: + def getClientSSLContext(self, certname: StrOrNone =None) -> ssl.SSLContext: ''' Returns an ssl.SSLContext appropriate for initiating a TLS session @@ -1360,7 +1360,7 @@ def getClientSSLContext(self, certname: StrOrNoneType =None) -> ssl.SSLContext: return sslctx - def getServerSSLContext(self, hostname: StrOrNoneType =None, caname: StrOrNoneType =None) -> ssl.SSLContext: + def getServerSSLContext(self, hostname: StrOrNone =None, caname: StrOrNone =None) -> ssl.SSLContext: ''' Returns an ssl.SSLContext appropriate to listen on a socket @@ -1392,7 +1392,7 @@ def snifunc(sslsock, sslname, origctx): return self._getServerSSLContext(hostname=hostname, caname=caname) - def getCrlPath(self, name: str) -> StrOrNoneType: + def getCrlPath(self, name: str) -> StrOrNone: for cdir in self.certdirs: path = s_common.genpath(cdir, 'crls', '%s.crl' % name) if os.path.isfile(path): @@ -1509,7 +1509,7 @@ def _genCertBuilder(self, name: str, pubkey: c_types.PublicKeyTypes) -> c_x509.C builder = builder.public_key(pubkey) return builder - def _genPkeyCsr(self, name: str, mode: str, outp: OutPutOrNoneType=None) -> bytes: + def _genPkeyCsr(self, name: str, mode: str, outp: OutPutOrNone=None) -> bytes: pkey = self._genPrivKey() @@ -1535,11 +1535,11 @@ def _genPkeyCsr(self, name: str, mode: str, outp: OutPutOrNoneType=None) -> byte return byts - def _getCaPath(self, cert: c_x509.Certificate) -> StrOrNoneType: + def _getCaPath(self, cert: c_x509.Certificate) -> StrOrNone: issuer = cert.issuer.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0] return self.getCaCertPath(issuer.value) - def _getPathBytes(self, path: str) -> BytesOrNoneType: + def _getPathBytes(self, path: str) -> BytesOrNone: if path is None: return None return s_common.getbytes(path) @@ -1548,7 +1548,7 @@ def _getPathJoin(self, *paths: str) -> str: '''Get the base certidr path + paths''' return s_common.genpath(self.certdirs[0], *paths) - def _loadCertPath(self, path: str) -> CertOrNoneType: + def _loadCertPath(self, path: str) -> CertOrNone: byts = self._getPathBytes(path) if byts: return self._loadCertByts(byts) @@ -1582,7 +1582,7 @@ def _loadCsrPath(self, path: str) -> Union[c_x509.CertificateSigningRequest | No def _loadCsrByts(self, byts: bytes) -> c_x509.CertificateSigningRequest: return c_x509.load_pem_x509_csr(byts) - def _loadKeyPath(self, path: str) -> PkeyOrNoneType: + def _loadKeyPath(self, path: str) -> PkeyOrNone: byts = self._getPathBytes(path) if byts: pkey = c_serialization.load_pem_private_key(byts, password=None) @@ -1591,7 +1591,7 @@ def _loadKeyPath(self, path: str) -> PkeyOrNoneType: raise s_exc.BadCertBytes(mesg=f'Key is {pkey.__class__.__name__}, expected a DSA or RSA key, {path=}', path=path) - def _loadP12Path(self, path: str) -> Pkcs12OrNoneType: + def _loadP12Path(self, path: str) -> Pkcs12OrNone: byts = self._getPathBytes(path) if byts: p12 = c_pkcs12.load_pkcs12(byts, password=None) @@ -1610,13 +1610,13 @@ def _saveCertTo(self, cert: c_x509.Certificate, *paths: str) -> str: def _certToByts(self, cert: c_x509.Certificate): return cert.public_bytes(encoding=c_serialization.Encoding.PEM) - def _pkeyToByts(self, pkey: PkeyType) -> bytes: + def _pkeyToByts(self, pkey: Pkey) -> bytes: return pkey.private_bytes(encoding=c_serialization.Encoding.PEM, format=c_serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=c_serialization.NoEncryption(), ) - def _savePkeyTo(self, pkey: PkeyType, *paths: str): + def _savePkeyTo(self, pkey: Pkey, *paths: str): path = self._getPathJoin(*paths) self._checkDupFile(path) diff --git a/synapse/tests/test_lib_certdir.py b/synapse/tests/test_lib_certdir.py index 3c30c05eda..668b2ed97a 100644 --- a/synapse/tests/test_lib_certdir.py +++ b/synapse/tests/test_lib_certdir.py @@ -24,7 +24,7 @@ class CertDirTest(s_t_utils.SynTest): @contextlib.contextmanager - def getCertDir(self): + def getCertDir(self) -> contextlib.AbstractContextManager[s_certdir.CertDir, None, None]: ''' Get a test CertDir object. @@ -38,7 +38,7 @@ def getCertDir(self): def basic_assertions(self, cdir: s_certdir.CertDir, cert: c_x509.Certificate, - key: s_certdir.PkeyType, + key: s_certdir.Pkey, cacert: c_x509.Certificate =None): ''' test basic certificate assumptions @@ -134,7 +134,7 @@ def basic_assertions(self, def host_assertions(self, cdir: s_certdir.CertDir, cert: c_x509.Certificate, - key: s_certdir.PkeyType, + key: s_certdir.Pkey, cacert: c_x509.Certificate = None): ''' test basic certificate assumptions for a host certificate @@ -180,7 +180,7 @@ def host_assertions(self, def user_assertions(self, cdir: s_certdir.CertDir, cert: c_x509.Certificate, - key: s_certdir.PkeyType, + key: s_certdir.Pkey, cacert: c_x509.Certificate = None): ''' test basic certificate assumptions for a user certificate @@ -224,7 +224,7 @@ def user_assertions(self, def p12_assertions(self, cdir: s_certdir.CertDir, cert: c_x509.Certificate, - key: s_certdir.PkeyType, + key: s_certdir.Pkey, p12: c_pkcs12.PKCS12KeyAndCertificates, cacert: c_x509.Certificate = None): ''' @@ -270,7 +270,7 @@ def p12_assertions(self, def code_assertions(self, cdir: s_certdir.CertDir, cert: c_x509.Certificate, - key: s_certdir.PkeyType, + key: s_certdir.Pkey, cacert: c_x509.Certificate = None ): reqbc = c_x509.BasicConstraints(ca=False, path_length=None) @@ -305,7 +305,7 @@ def code_assertions(self, def test_certdir_cas(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDiNew + with self.getCertDir() as cdir: caname = 'syntest' inter_name = 'testsyn-intermed' base = cdir._getPathJoin() @@ -341,7 +341,7 @@ def test_certdir_cas(self): self.basic_assertions(cdir, inter_cacert, inter_cakey, cacert=cacert) def test_certdir_hosts(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'syntest' hostname = 'visi.vertex.link' hostname_unsigned = 'unsigned.vertex.link' @@ -400,7 +400,7 @@ def test_certdir_hosts(self): self.len(64, chash) def test_certdir_users(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'syntest' username = 'visi@vertex.link' username_unsigned = 'unsigned@vertex.link' @@ -473,7 +473,7 @@ def test_certdir_users(self): self.raises(s_exc.NoSuchFile, cdir.genClientCert, username) # user crt def test_certdir_hosts_sans(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'syntest' cdir.genCaCert(caname) @@ -535,7 +535,7 @@ def test_certdir_hosts_sans(self): cdir.genHostCert(hostname, signas=caname, sans=sans) def test_certdir_hosts_csr(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'syntest' hostname = 'visi.vertex.link' @@ -558,7 +558,7 @@ def test_certdir_hosts_csr(self): self.basic_assertions(cdir, cert, key, cacert=cacert) def test_certdir_users_csr(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'syntest' username = 'visi@vertex.link' @@ -581,7 +581,7 @@ def test_certdir_users_csr(self): self.basic_assertions(cdir, cert, key, cacert=cacert) def test_certdir_importfile(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: with self.getTestDir() as testpath: # File doesn't exist @@ -622,7 +622,7 @@ def test_certdir_importfile(self): self.raises(s_exc.FileExists, cdir.importFile, srcpath, ftype) def test_certdir_valUserCert(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: cdir._getPathJoin() cdir.genCaCert('syntest') cdir.genCaCert('newp') @@ -687,7 +687,7 @@ def test_certdir_sslctx(self): async def test_certdir_codesign(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'The Vertex Project ROOT CA' immname = 'The Vertex Project Intermediate CA 00' codename = 'Vertex Build Pipeline' @@ -859,7 +859,7 @@ async def test_cortex_codesign(self): def test_certdir_save_load(self): - with self.getCertDir() as cdir: # type: s_certdir.CertDir + with self.getCertDir() as cdir: caname = 'TestCA' hostname = 'wee.wow.com' username = 'dude@wow.com' @@ -891,7 +891,7 @@ def test_certdir_save_load(self): u_path = cdir.getUserCertPath(username) co_path = cdir.getCodeCertPath(codename) - with self.getCertDir() as cdir2: # type: s_certdir.CertDir + with self.getCertDir() as cdir2: with s_common.genfile(ca_path) as fd: byts = fd.read() From f8bf84c6b3bd5c8e49cac3571c42617e5b072c8e Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 15 Feb 2024 14:27:57 +0000 Subject: [PATCH 24/31] utcnow is deprecated --- synapse/lib/certdir.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index abbde5a938..1537f4e2e9 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -136,7 +136,7 @@ def revoke(self, cert: c_x509.Certificate) -> None: except s_exc.BadCertVerify as e: raise s_exc.BadCertVerify(mesg=f'Failed to validate that certificate was signed by {self.name}') from e - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.UTC) builder = c_x509.RevokedCertificateBuilder() builder = builder.serial_number(cert.serial_number) builder = builder.revocation_date(now) @@ -161,7 +161,7 @@ def _verify(self, cert): def _save(self, timestamp: [datetime.datetime | None] =None) -> None: if timestamp is None: - timestamp = datetime.datetime.utcnow() + timestamp = datetime.datetime.now(datetime.UTC) self.crlbuilder = self.crlbuilder.last_update(timestamp) # We have to have a next updated time; but we set it to be >= the lifespan of our certificates in general. @@ -1502,7 +1502,7 @@ def _genCertBuilder(self, name: str, pubkey: c_types.PublicKeyTypes) -> c_x509.C c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), ])) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.UTC) builder = builder.not_valid_before(now) builder = builder.not_valid_after(now + TEN_YEARS_TD) # certificates are good for 10 years builder = builder.serial_number(int(s_common.guid(), 16)) From 55d96b128c569be5588a8d396ec1ddf214c24e70 Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Fri, 16 Feb 2024 10:48:04 -0500 Subject: [PATCH 25/31] Apply suggestions from code review Co-authored-by: Cisphyx --- synapse/lib/certdir.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 1537f4e2e9..04cbf9937f 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -255,7 +255,7 @@ def genCaCert(self, name: str, name: The name of the CA keypair. signas: The CA keypair to sign the new CA with. outp: The output buffer. - save: Safe the certificate and key to disk. + save: Save the certificate and key to disk. Examples: Make a CA named "myca":: @@ -310,7 +310,7 @@ def genHostCert(self, name: str, myhostkey, myhostcert = cdir.genHostCert('myhost') Returns: - Tuple containing the private key and certificate objects. Private key may be none when signing a CSR. + Tuple containing the private key and certificate objects. Private key may be None when signing a CSR. ''' if csr is None: prvkey = self._genPrivKey() @@ -645,7 +645,7 @@ def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | Non Args: byts: The bytes for the User Certificate. - cacerts: A tuple of CA Certificates to use for validating the user cert.. + cacerts: A tuple of CA Certificates to use for validating the user cert. Raises: BadCertVerify: If the certificate is not valid. @@ -756,7 +756,7 @@ def getCaCertPath(self, name: str) -> StrOrNone: mypath = cdir.getCACertPath('myca') Returns: - The path if exists. + The path, if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'cas', '%s.crt' % name) @@ -793,7 +793,7 @@ def getCaKeyPath(self, name: str) -> StrOrNone: mypath = cdir.getCAKeyPath('myca') Returns: - The path if exists. + The path, if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'cas', '%s.key' % name) @@ -833,7 +833,7 @@ def getClientCertPath(self, name: str) -> StrOrNone: mypath = cdir.getClientCertPath('myuser') Returns: - The path if exists. + The path, if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.p12' % name) @@ -854,7 +854,7 @@ def getHostCaPath(self, name: str) -> StrOrNone: mypath = cdir.getHostCaPath('myhost') Returns: - The path if exists. + The path, if exists. ''' cert = self.getHostCert(name) if cert is None: @@ -898,7 +898,7 @@ def getHostCertPath(self, name: str) -> StrOrNone: mypath = cdir.getHostCertPath('myhost') Returns: - The path if exists. + The path, if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'hosts', '%s.crt' % name) @@ -955,7 +955,7 @@ def getUserCaPath(self, name: str) -> StrOrNone: mypath = cdir.getUserCaPath('myuser') Returns: - The path if exists. + The path, if exists. ''' cert = self.getUserCert(name) if cert is None: @@ -993,7 +993,7 @@ def getUserCertPath(self, name: str) -> StrOrNone: mypath = cdir.getUserCertPath('myuser') Returns: - The path if exists. + The path, if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.crt' % name) @@ -1052,7 +1052,7 @@ def getUserKeyPath(self, name: str) -> StrOrNone: mypath = cdir.getUserKeyPath('myuser') Returns: - The path if exists. + The path, if exists. ''' for cdir in self.certdirs: path = s_common.genpath(cdir, 'users', '%s.key' % name) From e961b0da56995f1244cd7e4d692d26e045562c83 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Fri, 16 Feb 2024 19:03:18 +0000 Subject: [PATCH 26/31] Cleanup imports in test_lib_cell.py --- synapse/tests/test_lib_cell.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/synapse/tests/test_lib_cell.py b/synapse/tests/test_lib_cell.py index 962689625f..7537c19d86 100644 --- a/synapse/tests/test_lib_cell.py +++ b/synapse/tests/test_lib_cell.py @@ -35,9 +35,6 @@ import synapse.tools.backup as s_tools_backup import synapse.tests.utils as s_t_utils -from synapse.tests.utils import alist - -from OpenSSL import crypto # Defective versions of spawned backup processes def _sleeperProc(pipe, srcdir, dstdir, lmdbpaths, logconf): @@ -748,7 +745,7 @@ async def test_cell_nexuscull(self): self.true(await prox.cullNexsLog(offs)) # last entry in nexus log is cull - retn = await alist(cell.nexsroot.nexslog.iter(0)) + retn = await s_t_utils.alist(cell.nexsroot.nexslog.iter(0)) self.len(1, retn) self.eq(ind, retn[0][0]) self.eq('nexslog:cull', retn[0][1][1]) From 62f9ee290041ac8e613a452ab312ca91dd7404df Mon Sep 17 00:00:00 2001 From: epiphyte Date: Fri, 16 Feb 2024 19:06:15 +0000 Subject: [PATCH 27/31] cleanup code for consistency --- synapse/lib/certdir.py | 65 +++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 1537f4e2e9..c5c1af319e 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -78,7 +78,7 @@ def _initTLSServerCiphers(): for cipher in ctx.get_ciphers(): # pragma: no cover if cipher.get('protocol') not in ('TLSv1.2', 'TLSv1.3'): continue - if cipher.get('kea') == 'kx-rsa': # pragma: no cover + if cipher.get('kea') == 'kx-rsa': # pragma: no cover continue _ciphers.append(cipher) @@ -112,8 +112,8 @@ def __init__(self, cdir, name): self.path = self.certdir.genCrlPath(name) self.crlbuilder = c_x509.CertificateRevocationListBuilder().issuer_name(c_x509.Name([ - c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), - ])) + c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), + ])) if os.path.isfile(self.path): with io.open(self.path, 'rb') as fd: @@ -152,13 +152,13 @@ def _verify(self, cert): store = crypto.X509Store() store.add_cert(crypto.X509.from_cryptography(cacert)) store.set_flags(crypto.X509StoreFlags.PARTIAL_CHAIN) - ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert),) + ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert), ) try: ctx.verify_certificate() except crypto.X509StoreContextError as e: raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) from None - def _save(self, timestamp: [datetime.datetime | None] =None) -> None: + def _save(self, timestamp: [datetime.datetime | None] = None) -> None: if timestamp is None: timestamp = datetime.datetime.now(datetime.UTC) @@ -213,7 +213,7 @@ class CertDir: * CertDir does not currently support signing CA CSRs. ''' - def __init__(self, path: StrOrNone =None): + def __init__(self, path: StrOrNone = None): self.crypto_numbits = 4096 self.signing_digest = c_hashes.SHA256 @@ -246,8 +246,8 @@ def delCertPath(self, *path: str): def genCaCert(self, name: str, signas: StrOrNone = None, - outp: OutPutOrNone =None, - save: bool =True) -> PkeyAndCert: + outp: OutPutOrNone = None, + save: bool = True) -> PkeyAndCert: ''' Generates a CA keypair. @@ -291,8 +291,8 @@ def genCaCert(self, name: str, def genHostCert(self, name: str, signas: StrOrNone = None, outp: OutPutOrNone = None, - csr: PubKeyOrNone =None, - sans: StrOrNone =None, + csr: PubKeyOrNone = None, + sans: StrOrNone = None, save: bool = True) -> PkeyOrNoneAndCert: ''' Generates a host keypair. @@ -378,7 +378,7 @@ def genHostCert(self, name: str, return prvkey, cert - def genHostCsr(self, name: str, outp: OutPutOrNone =None) -> bytes: + def genHostCsr(self, name: str, outp: OutPutOrNone = None) -> bytes: ''' Generates a host certificate signing request. @@ -400,7 +400,7 @@ def genUserCert(self, name: str, signas: Optional[str | None] = None, outp: OutPutOrNone = None, - csr: PubKeyOrNone =None, + csr: PubKeyOrNone = None, save: bool = True) -> PkeyOrNoneAndCert: ''' Generates a user keypair. @@ -432,7 +432,8 @@ def genUserCert(self, critical=False, ) builder = builder.add_extension( - c_x509.KeyUsage(digital_signature=True, key_encipherment=False, data_encipherment=False, key_agreement=False, + c_x509.KeyUsage(digital_signature=True, key_encipherment=False, data_encipherment=False, + key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, content_commitment=False), critical=False, @@ -458,7 +459,7 @@ def genUserCert(self, return prvkey, cert - def genCodeCert(self, name: str, signas: StrOrNone =None, outp: OutPutOrNone =None, save: bool=True)\ + def genCodeCert(self, name: str, signas: StrOrNone = None, outp: OutPutOrNone = None, save: bool = True) \ -> PkeyAndCert: ''' Generates a code signing keypair. @@ -522,6 +523,7 @@ def getCodeCertPath(self, name: str) -> StrOrNone: path = s_common.genpath(cdir, 'code', f'{name}.crt') if os.path.isfile(path): return path + def getCodeKey(self, name: str) -> Union[s_rsa.PriKey | None]: path = self.getCodeKeyPath(name) @@ -534,7 +536,7 @@ def getCodeKey(self, name: str) -> Union[s_rsa.PriKey | None]: def getCodeCert(self, name: str) -> CertOrNone: path = self.getCodeCertPath(name) - if path is None: # pragma: no cover + if path is None: # pragma: no cover return None return self._loadCertPath(path) @@ -589,7 +591,7 @@ def _getCaCrls(self) -> List[c_x509.CertificateRevocationList]: for name in os.listdir(crlpath): - if not name.endswith('.crl'): # pragma: no cover + if not name.endswith('.crl'): # pragma: no cover continue fullpath = os.path.join(crlpath, name) @@ -599,7 +601,7 @@ def _getCaCrls(self) -> List[c_x509.CertificateRevocationList]: return crls - def genClientCert(self, name: str, outp: OutPutOrNone =None) -> None: + def genClientCert(self, name: str, outp: OutPutOrNone = None) -> None: ''' Generates a user PKCS #12 archive. @@ -639,7 +641,7 @@ def genClientCert(self, name: str, outp: OutPutOrNone =None) -> None: if outp is not None: outp.printf('client cert saved: %s' % (crtpath,)) - def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | None] =None) -> c_x509.Certificate: + def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | None] = None) -> c_x509.Certificate: ''' Validate the PEM encoded x509 user certificate bytes and return it. @@ -676,7 +678,7 @@ def valUserCert(self, byts: bytes, cacerts: Union[List[c_x509.Certificate] | Non raise s_exc.BadCertVerify(mesg=_unpackContextError(e)) return cert - def genUserCsr(self, name: str, outp: OutPutOrNone =None) -> bytes: + def genUserCsr(self, name: str, outp: OutPutOrNone = None) -> bytes: ''' Generates a user certificate signing request. @@ -735,7 +737,7 @@ def getCaCerts(self) -> List[c_x509.Certificate]: for name in os.listdir(path): - if not name.endswith('.crt'): # pragma: no cover + if not name.endswith('.crt'): # pragma: no cover continue full = s_common.genpath(cdir, 'cas', name) @@ -1070,7 +1072,8 @@ def getHostCsrPath(self, name: str) -> StrOrNone: path = s_common.genpath(cdir, 'hosts', '%s.csr' % name) if os.path.isfile(path): return path - def importFile(self, path: str, mode: str, outp: OutPutOrNone =None) -> None: + + def importFile(self, path: str, mode: str, outp: OutPutOrNone = None) -> None: ''' Imports certs and keys into the Synapse cert directory @@ -1232,9 +1235,9 @@ def signCertAs(self, builder: c_x509.CertificateBuilder, signas: str) -> c_x509. def signHostCsr(self, xcsr: c_x509.CertificateSigningRequest, signas: str, - outp: OutPutOrNone =None, - sans: StrOrNone =None, - save: bool =True) -> PkeyOrNoneAndCert: + outp: OutPutOrNone = None, + sans: StrOrNone = None, + save: bool = True) -> PkeyOrNoneAndCert: ''' Signs a host CSR with a CA keypair. @@ -1285,8 +1288,8 @@ def selfSignCert(self, builder: c_x509.CertificateBuilder, pkey: Pkey) -> c_x509 def signUserCsr(self, xcsr: c_x509.CertificateSigningRequest, signas: str, - outp: OutPutOrNone=None, - save: bool =True) -> PkeyOrNoneAndCert: + outp: OutPutOrNone = None, + save: bool = True) -> PkeyOrNoneAndCert: ''' Signs a user CSR with a CA keypair. @@ -1321,7 +1324,7 @@ def _loadCasIntoSSLContext(self, ctx): if name.endswith('.crt'): ctx.load_verify_locations(os.path.join(path, name)) - def getClientSSLContext(self, certname: StrOrNone =None) -> ssl.SSLContext: + def getClientSSLContext(self, certname: StrOrNone = None) -> ssl.SSLContext: ''' Returns an ssl.SSLContext appropriate for initiating a TLS session @@ -1360,7 +1363,7 @@ def getClientSSLContext(self, certname: StrOrNone =None) -> ssl.SSLContext: return sslctx - def getServerSSLContext(self, hostname: StrOrNone =None, caname: StrOrNone =None) -> ssl.SSLContext: + def getServerSSLContext(self, hostname: StrOrNone = None, caname: StrOrNone = None) -> ssl.SSLContext: ''' Returns an ssl.SSLContext appropriate to listen on a socket @@ -1509,12 +1512,12 @@ def _genCertBuilder(self, name: str, pubkey: c_types.PublicKeyTypes) -> c_x509.C builder = builder.public_key(pubkey) return builder - def _genPkeyCsr(self, name: str, mode: str, outp: OutPutOrNone=None) -> bytes: + def _genPkeyCsr(self, name: str, mode: str, outp: OutPutOrNone = None) -> bytes: pkey = self._genPrivKey() builder = c_x509.CertificateSigningRequestBuilder() - builder = builder.subject_name(c_x509.Name([c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name),])) + builder = builder.subject_name(c_x509.Name([c_x509.NameAttribute(c_x509.NameOID.COMMON_NAME, name), ])) builder = builder.add_extension(c_x509.BasicConstraints(ca=False, path_length=None), critical=True) request = builder.sign(pkey, c_hashes.SHA256()) @@ -1543,6 +1546,7 @@ def _getPathBytes(self, path: str) -> BytesOrNone: if path is None: return None return s_common.getbytes(path) + # def _getPathJoin(self, *paths: str) -> str: '''Get the base certidr path + paths''' @@ -1637,6 +1641,7 @@ def _saveP12To(self, byts: bytes, *paths: str): return path certdir = CertDir() + def getCertDir() -> CertDir: ''' Get the singleton CertDir instance. From eb2110042705b7289c9fbc92028a248cabd3c4bd Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Tue, 20 Feb 2024 11:37:59 -0500 Subject: [PATCH 28/31] Apply suggestions from code review Co-authored-by: Cisphyx --- synapse/lib/certdir.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index b3ac52f715..d66cbfac39 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -152,7 +152,7 @@ def _verify(self, cert): store = crypto.X509Store() store.add_cert(crypto.X509.from_cryptography(cacert)) store.set_flags(crypto.X509StoreFlags.PARTIAL_CHAIN) - ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert), ) + ctx = crypto.X509StoreContext(store, crypto.X509.from_cryptography(cert)) try: ctx.verify_certificate() except crypto.X509StoreContextError as e: @@ -790,7 +790,7 @@ def getCaKeyPath(self, name: str) -> StrOrNone: name: The name of the CA keypair. Examples: - Get the path to the private key for the CA "myca": + Get the path to the private key for the CA "myca":: mypath = cdir.getCAKeyPath('myca') @@ -810,7 +810,7 @@ def getClientCert(self, name: str) -> Pkcs12OrNone: name: The name of the user keypair. Examples: - Get the PKCS12 object for the user "myuser": + Get the PKCS12 object for the user "myuser":: mypkcs12 = cdir.getClientCert('myuser') @@ -830,7 +830,7 @@ def getClientCertPath(self, name: str) -> StrOrNone: name: The name of the client keypair. Examples: - Get the path to the client certificate for "myuser": + Get the path to the client certificate for "myuser":: mypath = cdir.getClientCertPath('myuser') @@ -895,7 +895,7 @@ def getHostCertPath(self, name: str) -> StrOrNone: name: The name of the host keypair. Examples: - Get the path to the host certificate for the host "myhost": + Get the path to the host certificate for the host "myhost":: mypath = cdir.getHostCertPath('myhost') @@ -1549,7 +1549,7 @@ def _getPathBytes(self, path: str) -> BytesOrNone: # def _getPathJoin(self, *paths: str) -> str: - '''Get the base certidr path + paths''' + '''Get the base certdir path + paths''' return s_common.genpath(self.certdirs[0], *paths) def _loadCertPath(self, path: str) -> CertOrNone: From 7d11c67cc9b8a7172d71a1fb6bf8bb5b77b3f454 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 22 Feb 2024 14:42:25 +0000 Subject: [PATCH 29/31] Update cryptography dep --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5090833b89..481768f3d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ 'pyOpenSSL>=24.0.0,<25.3.0', - 'cryptography>=42.0.0,<43.0.0', + 'cryptography>=42.0.4,<43.0.0', 'msgpack>=1.0.5,<1.1.0', 'xxhash>=1.4.4,<3.5.0', 'lmdb>=1.2.1,<1.5.0', diff --git a/requirements.txt b/requirements.txt index f735ab6704..b4fc3eab04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,4 @@ beautifulsoup4[html5lib]>=4.11.1,<5.0.0 # pin. Cryptography also vendors a copy of OpenSSL, so it needs to be able to # have a minimum version bumped in the event of a OpenSSL vulnerability that # needs to be patched. -cryptography>=42.0.0,<43.0.0 +cryptography>=42.0.4,<43.0.0 From 99b72579dec038d19c334036bc39d10f75f75417 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 22 Feb 2024 19:39:00 +0000 Subject: [PATCH 30/31] Correct pyopenssl constraint; use s_const.year --- pyproject.toml | 2 +- requirements.txt | 2 +- synapse/lib/certdir.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 481768f3d4..88a08917b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ 'Operating System :: POSIX :: Linux', ] dependencies = [ - 'pyOpenSSL>=24.0.0,<25.3.0', + 'pyOpenSSL>=24.0.0,<25.0.0', 'cryptography>=42.0.4,<43.0.0', 'msgpack>=1.0.5,<1.1.0', 'xxhash>=1.4.4,<3.5.0', diff --git a/requirements.txt b/requirements.txt index b4fc3eab04..5fecb30339 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyOpenSSL>=24.0.0,<25.3.0 +pyOpenSSL>=24.0.0,<25.0.0 msgpack>=1.0.5,<1.1.0 xxhash>=1.4.4,<3.5.0 lmdb>=1.2.1,<1.5.0 diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index d66cbfac39..637be68daf 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -14,6 +14,7 @@ import synapse.exc as s_exc import synapse.common as s_common +import synapse.lib.const as s_const import synapse.lib.output as s_output import synapse.lib.crypto.rsa as s_rsa @@ -37,8 +38,8 @@ NSCERTTYPE_SERVER = b'\x03\x02\x06@' # server NSCERTTYPE_OBJSIGN = b'\x03\x02\x04\x10' # objsign -TEN_YEARS = 10 * 365 * 24 * 60 * 60 # 10 years in seconds -TEN_YEARS_TD = datetime.timedelta(seconds=TEN_YEARS) +TEN_YEARS = 10 * s_const.year # 10 years in milliseconds +TEN_YEARS_TD = datetime.timedelta(milliseconds=TEN_YEARS) StrOrNone = Union[str | None] BytesOrNone = Union[bytes | None] From 80db8d4a6cff858c31b4f42986b69c9fd9cedd63 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 22 Feb 2024 19:43:48 +0000 Subject: [PATCH 31/31] Remove the one use of optional --- synapse/lib/certdir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/lib/certdir.py b/synapse/lib/certdir.py index 637be68daf..7cdb4ca53e 100644 --- a/synapse/lib/certdir.py +++ b/synapse/lib/certdir.py @@ -8,7 +8,7 @@ import datetime import collections -from typing import List, Optional, Tuple, Union +from typing import List, Tuple, Union from OpenSSL import crypto # type: ignore @@ -399,7 +399,7 @@ def genHostCsr(self, name: str, outp: OutPutOrNone = None) -> bytes: def genUserCert(self, name: str, - signas: Optional[str | None] = None, + signas: StrOrNone = None, outp: OutPutOrNone = None, csr: PubKeyOrNone = None, save: bool = True) -> PkeyOrNoneAndCert: