From 640884e7dff950f455670fc86752dd7ce2878701 Mon Sep 17 00:00:00 2001 From: Nolan Rumble Date: Wed, 2 Aug 2023 18:50:49 -0700 Subject: [PATCH 1/2] Version 1.08 See CHANGELOG.md for details. --- CHANGELOG.md | 5 +++ README.md | 40 ++++++++++++++----- generate-certificate.py | 85 ++++++++++++++++++++++++++++++++++------- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128df93..a0bbb42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2023-08-02 +## Version 1.08 +* Adding enhancement for [Subject Alternative Names](https://github.com/TheScriptGuy/certificateAuthenticationGenerator/issues/8) +* Inadvertently identifying (and now fixed) a flaw in my Client Certificate Authentication generator. + # 2023-05-31 ## Version 1.07 * Adjusting code to align with better coding practices. diff --git a/README.md b/README.md index 7f7441b..ffa8fd9 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,16 @@ You can change the company name by using the `--companyName` argument. # Requirements pyopenssl must be installed. To install: ```bash -$ python3 -m pip install cryptography==40.0.2 datetime +$ python3 -m pip install cryptography==40.0.2 datetime pyasn1 idna ``` # Help ```bash $ python3 generate-certificate.py -h usage: generate-certificate.py [-h] [--companyName COMPANYNAME] [--generateRootCA] [--generateClientCertificate] [--generatePKCS12] - [--nonRestrictiveRootCA] [--ecc] [--removeAllCertsAndKeys] [--windowsInstallation] + [--nonRestrictiveRootCA] [--ecc] [--dnsName] [--userPrincipalName] [--removeAllCertsAndKeys] [--windowsInstallation] -Certificate Generation v1.07 +Certificate Generation v1.08 options: -h, --help show this help message and exit @@ -42,6 +42,8 @@ options: --nonRestrictiveRootCA Remove Root CA extensions. USE WITH CAUTION. --ecc Use Elliptic Curves in preference to RSA. + --dnsName Add a Subject Alternative Name (SAN) for the DNS hostname. + --userPrincipalName Add a Subject Alternative Name (SAN) for the Windows User Principal Name (UPN). --removeAllCertsAndKeys Removes all files matching wildcard *.crt, *.key, *.p12. USE WITH CAUTION. --windowsInstallation @@ -125,6 +127,21 @@ To install certificate into Local Machine certificate store: C:\>certutil -importpfx -f -Enterprise -p thisisnotreallyapassword client-cert-test-inc.p12 NoExport ``` +## Generate Client Certificate with DNSName or userPrincipalName in Subject Alternative Name (SAN) +Make sure to edit the field first (under `def certificateMetaData`): +certificateInfo['ClientAuthentication']['oid']['subjectAlternativeName']['DNSName'] +```bash +$ python3 generate-certificate.py --companyName "Test123,. Inc" --generateClientCertificate --generatePKCS12 --dnsName +``` +OR + +Make sure to edit the field first (under `def certificateMetaData`): +certificateInfo['ClientAuthentication']['oid']['subjectAlternativeName']['userPrincipalName'] +```bash +$ python3 generate-certificate.py --companyName "Test123,. Inc" --generateClientCertificate --generatePKCS12 --userPrincipalName +``` + + ## Remove files generated by script To remove all files generated by the script ```bash @@ -154,6 +171,7 @@ My recommendation is to leave the following fields: If you'd like to edit how the certificates are generated, you can edit this dict within `def certificateMetaData`: ```python + # Root Certificate Authority information. Edit at your own risk. certificateInfo["RootCA"] = { "oid": { "CN": args.companyName + " Root CA", @@ -163,7 +181,7 @@ If you'd like to edit how the certificates are generated, you can edit this dict "stateOrProvince": None, "organizationName": None, "countryName": None, - "domainComponent": [None] + "domainComponent": [None], }, "rootCAFileName": rootCAFileName, "rootCAPublicKey": f"{rootCAFileName}.crt", @@ -177,11 +195,11 @@ If you'd like to edit how the certificates are generated, you can edit this dict }, "ecc": { "curve": "secp256r1", - "digest": "sha512" + "digest": "sha512", }, "extensions": { "keyUsage": ["digitalSignature", "nonRepudiation", "keyCertSign"], - "extendedKeyUsage": ["clientAuth"] + "extendedKeyUsage": ["clientAuth"], } } @@ -194,7 +212,11 @@ If you'd like to edit how the certificates are generated, you can edit this dict "stateOrProvince": None, "organizationName": None, "countryName": None, - "domainComponent": [None] + "domainComponent": [None], + "subjectAlternativeName": { + "DNSName": None, + "userPrincipalName": None, + } }, "clientCertificatePublicKey": f"{clientCertificateFileName}.crt", "clientCertificatePrivateKey": f"{clientCertificateFileName}.key", @@ -207,11 +229,11 @@ If you'd like to edit how the certificates are generated, you can edit this dict }, "ecc": { "curve": "secp256r1", - "digest": "sha256" + "digest": "sha256", }, "extensions": { "keyUsage": ["digitalSignature", "nonRepudiation"], - "extendedKeyUsage": ["clientAuth"] + "extendedKeyUsage": ["clientAuth"], } } ``` diff --git a/generate-certificate.py b/generate-certificate.py index ad7f2a4..080de88 100644 --- a/generate-certificate.py +++ b/generate-certificate.py @@ -1,10 +1,13 @@ # Description: Create a Root CA with a client Authentication certificate that's signed by the Root CA. # Author: TheScriptGuy -# Last modified: 2023-05-31 -# Version: 1.07 +# Last modified: 2023-08-02 +# Version: 1.08 from cryptography import x509 +from pyasn1.type import char +from pyasn1.codec.der import encoder as der_encoder + from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa, ec @@ -16,8 +19,9 @@ import os import argparse import random +import idna -scriptVersion = "1.07" +scriptVersion = "1.08" def certificateMetaData(): @@ -43,7 +47,7 @@ def certificateMetaData(): "stateOrProvince": None, "organizationName": None, "countryName": None, - "domainComponent": [None] + "domainComponent": [None], }, "rootCAFileName": rootCAFileName, "rootCAPublicKey": f"{rootCAFileName}.crt", @@ -57,11 +61,11 @@ def certificateMetaData(): }, "ecc": { "curve": "secp256r1", - "digest": "sha512" + "digest": "sha512", }, "extensions": { "keyUsage": ["digitalSignature", "nonRepudiation", "keyCertSign"], - "extendedKeyUsage": ["clientAuth"] + "extendedKeyUsage": ["clientAuth"], } } @@ -74,7 +78,11 @@ def certificateMetaData(): "stateOrProvince": None, "organizationName": None, "countryName": None, - "domainComponent": [None] + "domainComponent": [None], + "subjectAlternativeName": { + "DNSName": None, + "userPrincipalName": None, + } }, "clientCertificatePublicKey": f"{clientCertificateFileName}.crt", "clientCertificatePrivateKey": f"{clientCertificateFileName}.key", @@ -87,11 +95,11 @@ def certificateMetaData(): }, "ecc": { "curve": "secp256r1", - "digest": "sha256" + "digest": "sha256", }, "extensions": { "keyUsage": ["digitalSignature", "nonRepudiation"], - "extendedKeyUsage": ["clientAuth"] + "extendedKeyUsage": ["clientAuth"], } } @@ -122,6 +130,12 @@ def parseArguments(): parser.add_argument('--ecc', action='store_true', help='Use Elliptic Curves in preference to RSA.') + parser.add_argument('--dnsName', action='store_true', + help='Add a Subject Alternative Name (SAN) for the DNS hostname.') + + parser.add_argument('--userPrincipalName', action='store_true', + help='Add a Subject Alternative Name (SAN) for the Windows User Principal Name (UPN).') + parser.add_argument('--removeAllCertsAndKeys', action='store_true', help='Removes all files matching wildcard *.crt, *.key, *.p12. USE WITH CAUTION.') @@ -134,10 +148,10 @@ def parseArguments(): def printDisclaimer(): """Disclaimer for using the certificates.""" - print("----------------------------------------------------------------------------") + print("-" * 76) print("DISCLAIMER:") print("These files are not meant for production environments. Use at your own risk.") - print("----------------------------------------------------------------------------") + print("-" * 76) def printWindowsInstallationInstructions( @@ -145,7 +159,7 @@ def printWindowsInstallationInstructions( __p12Password: str ) -> None: """Display the installation instructions for Windows.""" - print("----------------------------------------------------------------------------") + print("-" * 76) print("Windows Installation (from the directory where files are stored):") print("To install Client Authentication certificate into User certificate store (in both cases, click yes to install Root CA as well):") print(f"C:\\>certutil -importpfx -f -user -p {__p12Password} {__certificateInfo['ClientAuthentication']['clientCertificatePKCS12']} NoExport") @@ -303,7 +317,7 @@ def create_client_certificate(__certificateMetaData: dict) -> None: clientPublicKey = clientPrivateKey.public_key() clientNameAttributes = CryptographySupport.CryptographySupport.build_name_attribute(__certificateMetaData['ClientAuthentication']) - + clientCertificateBuilder = x509.CertificateBuilder() clientCertificateBuilder = clientCertificateBuilder.subject_name(x509.Name(clientNameAttributes)) @@ -320,7 +334,7 @@ def create_client_certificate(__certificateMetaData: dict) -> None: # Create a list of extensions to be added to certificate. clientCertificateBuilder = clientCertificateBuilder.add_extension( - x509.BasicConstraints(ca=True, path_length=0), critical=True + x509.BasicConstraints(ca=False, path_length=None), critical=True ) # Add extended key usage extensions to the certificate @@ -329,6 +343,42 @@ def create_client_certificate(__certificateMetaData: dict) -> None: x509.ExtendedKeyUsage(clientCertificateExtendedKeyUsage), critical=True ) + # Add Subject Alternative Name extensions + if args.dnsName: + # The DNSName needs to be attached to the Subject Alternative Name. + + # First check to see if the dnsName has been defined in the dict. + if __certificateMetaData['ClientAuthentication']['oid']['subjectAlternativeName']['DNSName'] is not None: + __dnsName = __certificateMetaData['ClientAuthentication']['oid']['subjectAlternativeName']['DNSName'] + __a_dnsName = idna.encode(__dnsName).decode('ascii') + clientCertificateBuilder = clientCertificateBuilder.add_extension( + x509.SubjectAlternativeName( + [x509.DNSName(__a_dnsName)] + ), critical=False + ) + else: + # The required key: value pair was not set. Print error message and exit. + print(f"The required key: value pair was not set for DNSName.") + sys.exit(1) + + if args.userPrincipalName: + # The User Principal Name needs to be attached to the Subject Alternative Name. + if __certificateMetaData['ClientAuthentication']['oid']['subjectAlternativeName']['userPrincipalName'] is not None: + # UPN field + upn_value = __certificateMetaData['ClientAuthentication']['oid']['subjectAlternativeName']['userPrincipalName'] + upn_value = der_encoder.encode(char.UTF8String(upn_value)) # ASN.1 DER encoding + upn_field = x509.OtherName(x509.oid.ObjectIdentifier('1.3.6.1.4.1.311.20.2.3'), upn_value) + + clientCertificateBuilder = clientCertificateBuilder.add_extension( + x509.SubjectAlternativeName( + [upn_field] + ), critical=False + ) + else: + # The required key: value pair was not set. Print error message and exit. + print(f"The required key: value pair was not set for userPrincipalName.") + sys.exit(1) + # Load Root CA Key with open(__certificateMetaData["RootCA"]["rootCAPrivateKey"], "rb") as f_rootCAKeyFile: rootCAkeyPEM = serialization.load_pem_private_key(f_rootCAKeyFile.read(), password=None) @@ -391,6 +441,13 @@ def main(): print("Missing --generateRootCA or --generateClientCertificate Argument.") sys.exit(1) + # First check to see if only one argument was passed + # Can only be --dnsName or --userPrincipalName, but not both. Exit if true. + if args.dnsName and args.userPrincipalName: + # Print an error message and exit. + print(f"Please use either --dnsName or --userPrincipalName, but not both.") + sys.exit(1) + # Check to see if Root CA needs to be generated. if args.generateRootCA and args.companyName: createRootCA(myCertMetaData) From 9a6d7bed617c31a13e2a69b5c95a541123e84f5a Mon Sep 17 00:00:00 2001 From: Nolan Rumble Date: Wed, 2 Aug 2023 18:53:41 -0700 Subject: [PATCH 2/2] Update generate-certificate.py --- generate-certificate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generate-certificate.py b/generate-certificate.py index 080de88..3c84484 100644 --- a/generate-certificate.py +++ b/generate-certificate.py @@ -358,7 +358,7 @@ def create_client_certificate(__certificateMetaData: dict) -> None: ) else: # The required key: value pair was not set. Print error message and exit. - print(f"The required key: value pair was not set for DNSName.") + print("The required key: value pair was not set for DNSName.") sys.exit(1) if args.userPrincipalName: @@ -376,7 +376,7 @@ def create_client_certificate(__certificateMetaData: dict) -> None: ) else: # The required key: value pair was not set. Print error message and exit. - print(f"The required key: value pair was not set for userPrincipalName.") + print("The required key: value pair was not set for userPrincipalName.") sys.exit(1) # Load Root CA Key @@ -445,7 +445,7 @@ def main(): # Can only be --dnsName or --userPrincipalName, but not both. Exit if true. if args.dnsName and args.userPrincipalName: # Print an error message and exit. - print(f"Please use either --dnsName or --userPrincipalName, but not both.") + print("Please use either --dnsName or --userPrincipalName, but not both.") sys.exit(1) # Check to see if Root CA needs to be generated.