Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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"],
}
}

Expand All @@ -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",
Expand All @@ -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"],
}
}
```
85 changes: 71 additions & 14 deletions generate-certificate.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,8 +19,9 @@
import os
import argparse
import random
import idna

scriptVersion = "1.07"
scriptVersion = "1.08"


def certificateMetaData():
Expand All @@ -43,7 +47,7 @@ def certificateMetaData():
"stateOrProvince": None,
"organizationName": None,
"countryName": None,
"domainComponent": [None]
"domainComponent": [None],
},
"rootCAFileName": rootCAFileName,
"rootCAPublicKey": f"{rootCAFileName}.crt",
Expand All @@ -57,11 +61,11 @@ def certificateMetaData():
},
"ecc": {
"curve": "secp256r1",
"digest": "sha512"
"digest": "sha512",
},
"extensions": {
"keyUsage": ["digitalSignature", "nonRepudiation", "keyCertSign"],
"extendedKeyUsage": ["clientAuth"]
"extendedKeyUsage": ["clientAuth"],
}
}

Expand All @@ -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",
Expand All @@ -87,11 +95,11 @@ def certificateMetaData():
},
"ecc": {
"curve": "secp256r1",
"digest": "sha256"
"digest": "sha256",
},
"extensions": {
"keyUsage": ["digitalSignature", "nonRepudiation"],
"extendedKeyUsage": ["clientAuth"]
"extendedKeyUsage": ["clientAuth"],
}
}

Expand Down Expand Up @@ -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.')

Expand All @@ -134,18 +148,18 @@ 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(
__certificateInfo: dict,
__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")
Expand Down Expand Up @@ -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))

Expand All @@ -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
Expand All @@ -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("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("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)
Expand Down Expand Up @@ -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("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)
Expand Down