|
| 1 | +# Import the required modules |
| 2 | + |
| 3 | +import pkcs11 |
| 4 | +import os |
| 5 | +import sys |
| 6 | +import time |
| 7 | +import re |
| 8 | +import subprocess |
| 9 | + |
| 10 | +# Create banner. |
| 11 | + |
| 12 | +def create_banner(text, width=150, border_char='*'): |
| 13 | + lines = text.strip().split('\n') |
| 14 | + border = border_char * width |
| 15 | + |
| 16 | + print(border) |
| 17 | + for line in lines: |
| 18 | + print(f"{border_char} {line.ljust(width - 4)} {border_char}") |
| 19 | + print(border) |
| 20 | + |
| 21 | +banner_text = """ |
| 22 | +
|
| 23 | +
|
| 24 | +DISCLAIMER: |
| 25 | +This code is provided for example purposes only. |
| 26 | +It is not intended for production use. It has not been tested for security. |
| 27 | +nCipher disclaims all warranties, express or implied, including without |
| 28 | +limitation, any implied warranties of fitness of this code for any |
| 29 | +particular purpose. nCipher shall not be liable for any damages, however |
| 30 | +caused, arising out of any use of this code. |
| 31 | +
|
| 32 | +Description & Usage: |
| 33 | +
|
| 34 | +Creates or uses an exsisting an RSA Key Pair on an nShield HSM device |
| 35 | +Wraps the RSA private key with an exisiting or newly generated AES wrapping key. |
| 36 | +File is exported in encrypted binary format. |
| 37 | +Generates a CSR using the public key via OpenSSL with nfkm engine. |
| 38 | +
|
| 39 | + * Enter the RSA key label (e.g., mykey) |
| 40 | + * Enter the AES wrapping key label (e.g., wrappingkey) |
| 41 | + * Enter the token label (e.g., loadshared accelerator) |
| 42 | + * Enter the token pin |
| 43 | + * Enter the key size (e.g., 2048) |
| 44 | + * Do you want the public key to be a wrapping key? (y/n) |
| 45 | + * Public key is generated or exisiting key is used |
| 46 | + * Private key is generated or exisiting key is used |
| 47 | + * AES Secret key is generated or exisiting key is used |
| 48 | + * RSA Private key is wrapped |
| 49 | + * Wrapped key material can be saved to file (custom path can be given or default is current working directory) |
| 50 | + * CSR is generated using the public key |
| 51 | + * CSR is signed using the private key |
| 52 | + * CSR is saved as file (custom path can be given or default is current working directory) |
| 53 | +
|
| 54 | +""" |
| 55 | +# Create the banner |
| 56 | +create_banner(banner_text) |
| 57 | + |
| 58 | +# Functions |
| 59 | + |
| 60 | +def extract_appname_ident(key_label): |
| 61 | + """ |
| 62 | + Extracts a specific value (e.g., pkcs11_hashvalue####) from the command output |
| 63 | + based on the provided key label. |
| 64 | +
|
| 65 | + Args: |
| 66 | + key_label (str): The key label to search for in the command output. |
| 67 | +
|
| 68 | + Returns: |
| 69 | + str: The extracted value if found, or None if not found. |
| 70 | + """ |
| 71 | + try: |
| 72 | + # Get OS type to determine the command to run |
| 73 | + os_type = sys.platform |
| 74 | + |
| 75 | + if os_type == 'win32' or os_type == 'cygwin': |
| 76 | + output = subprocess.check_output( |
| 77 | + 'set PATH=%PATH%;C:\\Program Files\\nCipher\\nfast\\bin && set PATH=%PATH%;C:\\Program Files\\nCipher\\nfast\\openssl && nfkminfo -l', |
| 78 | + shell=True, text=True |
| 79 | + ) |
| 80 | + print('Available keys:') |
| 81 | + print(output) |
| 82 | + elif os_type == 'linux' or os_type == 'linux2': |
| 83 | + output = subprocess.check_output(['export PATH=$PATH:/opt/nfast/openssl/bin/ && /opt/nfast/bin/nfkminfo -l'], text=True, shell=True) |
| 84 | + print('Available keys:') |
| 85 | + print(output) |
| 86 | + else: |
| 87 | + print("Unsupported OS type") |
| 88 | + return None |
| 89 | + except subprocess.CalledProcessError as e: |
| 90 | + print("Error executing nfkminfo -l:", e) |
| 91 | + return None |
| 92 | + |
| 93 | + # Regex pattern to match and extract `pkcs11_*` for the given key label |
| 94 | + pattern = rf"key_(pkcs11_[a-zA-Z0-9]+).*`{re.escape(key_label)}'" |
| 95 | + |
| 96 | + # Search for the key label in the command output |
| 97 | + match = re.search(pattern, output) |
| 98 | + if match: |
| 99 | + # Extract the `pkcs11_*` portion |
| 100 | + appname_ident = match.group(1) |
| 101 | + print(f"Extracted appname_ident for '{key_label}': {appname_ident}") |
| 102 | + return appname_ident |
| 103 | + else: |
| 104 | + print(f"No matching key found for label: {key_label}") |
| 105 | + return None |
| 106 | + |
| 107 | + |
| 108 | +def get_user_input(prompt, default=None): |
| 109 | + """Helper function for user input with an optional default value.""" |
| 110 | + value = input(f"{prompt} [{default}]: ").strip() |
| 111 | + return value if value else default |
| 112 | + |
| 113 | +def format_key_output_rsa(key, key_type): |
| 114 | + '''Format the RSA key output for display''' |
| 115 | + |
| 116 | + key_info = f"{key_type} Key:\n" |
| 117 | + key_info += f" Label: {getattr(key, 'label', 'N/A')}\n" |
| 118 | + key_info += f" Key Size: {getattr(key, 'key_size', MODULUS_BITS)}-bit\n" |
| 119 | + key_info += f" Key Type: {key.__class__.__name__}\n" |
| 120 | + key_info += f" Exponent: {getattr(key, 'public_exponent', '65537')}\n" |
| 121 | + |
| 122 | + return key_info |
| 123 | + |
| 124 | +def format_key_output_aes(key, key_type): |
| 125 | + |
| 126 | + '''Format the AES key output for display''' |
| 127 | + |
| 128 | + key_info = f"{key_type} Key:\n" |
| 129 | + key_info += f" Label: {getattr(key, 'label', 'N/A')}\n" |
| 130 | + key_info += f" Key Size: {getattr(key, 'key_length',)}-bit\n" |
| 131 | + key_info += f" Key Type: {key.__class__.__name__}\n" |
| 132 | + |
| 133 | + return key_info |
| 134 | + |
| 135 | +def sanitize_filename(filename): |
| 136 | + # Remove invalid characters for Windows filenames |
| 137 | + return re.sub(r'[<>:"/\\|?*]', '_', filename) |
| 138 | + |
| 139 | + |
| 140 | +# If you're using a virtual environment, make sure pkcs11 is installed there |
| 141 | +# You can use pip list to see the installed packages |
| 142 | +# Install python-pkcs11 by executing pip install python-pkcs11. |
| 143 | + |
| 144 | +# If there's a problem with your Python installation, you might need to reinstall Python or the python module. |
| 145 | + |
| 146 | +# This script is used to generate a new RSA keypair on a nShield HSM Edge device. |
| 147 | + |
| 148 | +# The following environment variables must be set: |
| 149 | +# - PKCS11_MODULE_PATH: the path to the pkcs11 module |
| 150 | +# - PKCS11_TOKEN_LABEL: the label of the token to use |
| 151 | +# - PKCS11_PIN: the pin of the token to use |
| 152 | +# - add %NFAS_HOME%\bin to your PATH environment variable. |
| 153 | +# (Windows) Go to Settings > Environment Variables > System Variables > Path > Edit > New > %NFAST_HOME%\bin = C:\Program Files\nCipher\nfast\bin |
| 154 | +# (Linux) export PATH=$PATH:/opt/nfast/bin |
| 155 | + |
| 156 | +# Set the required environment variables. |
| 157 | + |
| 158 | +os.environ['PKCS11_MODULE_PATH'] = '/opt/nfast/toolkits/pkcs11/libcknfast.so' |
| 159 | +os.environ['CKNFAST_LOADSHARING'] = '1' |
| 160 | +os.environ['CKNFAST_FAKE_ACCELERATOR_LOGIN'] = '1' |
| 161 | +#os.environ['NFAST_HOME'] = 'C:\\Program Files\\nCipher\\nfast\\' |
| 162 | + |
| 163 | +# Defining the pkcs11 module library |
| 164 | +lib = pkcs11.lib(os.environ['PKCS11_MODULE_PATH']) |
| 165 | + |
| 166 | + |
| 167 | +# The following environment variables are optional: |
| 168 | +# They can also be placed in the cknfastrc file in the nfast directory. |
| 169 | + |
| 170 | +#os.environ['CKNFAST_OVERRIDE_SECURITY_ASSURANCE'] = 'unwrap_mech;tokenkeys' |
| 171 | +#os.environ['CKNFAST_DEBUG'] = '10' |
| 172 | +#os.environ['CKNFAST_DEBUGFILE'] = '/opt/nfast/pkcs11_debug.log' |
| 173 | + |
| 174 | + |
| 175 | +# Prompt for the key label |
| 176 | +PKCS11_KEY_LABEL = get_user_input("Enter the key label: ", default="rsa_key") |
| 177 | + |
| 178 | +# Prompt for the wrapping key label |
| 179 | +PKCS11_WRAPPING_KEY_LABEL = get_user_input("Enter the wrapping key label: ", default="wrapping_key") |
| 180 | + |
| 181 | +# Prompt for the token label |
| 182 | +PKCS11_TOKEN_LABEL = get_user_input("Enter the token label: ", default="loadshared accelerator") |
| 183 | + |
| 184 | +# Prompt for the pin |
| 185 | +PKCS11_PIN = get_user_input("Enter the token pin: ", default="1234") |
| 186 | + |
| 187 | +# Get the token using the provided label |
| 188 | +token = lib.get_token(token_label=PKCS11_TOKEN_LABEL) |
| 189 | + |
| 190 | +# Prompt for the key size |
| 191 | +key_size = get_user_input("Enter the key size (e.g., 2048): ", default=2048) |
| 192 | +MODULUS_BITS = key_size |
| 193 | + |
| 194 | +# Check if the key size is valid |
| 195 | +if key_size not in [2048, 4096]: |
| 196 | + sys.exit('Error: Key size must be 2048 or 4096') |
| 197 | + |
| 198 | +# Prompt to ask if user wants pubkey to be wrapping key. |
| 199 | +WRAPPING_KEY = get_user_input("Do you want the public key to be a wrapping key? (y/n): ", default="n").lower() == 'y' |
| 200 | + |
| 201 | +# The following templates are used to generate a keypair on the HSM. Modify as needed. |
| 202 | + |
| 203 | +public_key_template = {pkcs11.Attribute.TOKEN: True, |
| 204 | + pkcs11.Attribute.PUBLIC_EXPONENT: 65537, # Public exponent is always 65537. |
| 205 | + pkcs11.Attribute.MODULUS_BITS: MODULUS_BITS, |
| 206 | + pkcs11.Attribute.WRAP: WRAPPING_KEY, |
| 207 | + pkcs11.Attribute.VERIFY: True, |
| 208 | + pkcs11.Attribute.MODIFIABLE: True, |
| 209 | + pkcs11.Attribute.ENCRYPT: True, |
| 210 | + |
| 211 | + } |
| 212 | + |
| 213 | +private_key_template = {pkcs11.Attribute.TOKEN: True, |
| 214 | + pkcs11.Attribute.PRIVATE: True, |
| 215 | + pkcs11.Attribute.SENSITIVE: True, |
| 216 | + pkcs11.Attribute.UNWRAP: True, |
| 217 | + pkcs11.Attribute.MODIFIABLE: True, |
| 218 | + pkcs11.Attribute.EXTRACTABLE: True, |
| 219 | + pkcs11.Attribute.DECRYPT: True, |
| 220 | + pkcs11.Attribute.WRAP_WITH_TRUSTED: False, |
| 221 | + pkcs11.Attribute.SIGN: True, |
| 222 | + |
| 223 | +} |
| 224 | + |
| 225 | +# Open a session with the token |
| 226 | +with token.open(rw=True, user_pin=PKCS11_PIN) as session: |
| 227 | + try: |
| 228 | + # Verify if key exists. |
| 229 | + key = session.get_key(object_class=pkcs11.ObjectClass.PRIVATE_KEY, label=PKCS11_KEY_LABEL) |
| 230 | + |
| 231 | + print(f"Key pair with label '{PKCS11_KEY_LABEL}' already exists, wrapping private key portion instead...") |
| 232 | + time.sleep(2) |
| 233 | + |
| 234 | + # Store the private key in a variable |
| 235 | + private = key |
| 236 | + |
| 237 | + except pkcs11.exceptions.NoSuchKey: |
| 238 | + print(f"Key with label '{PKCS11_KEY_LABEL}' does not exist. Generating a new key pair... \n") |
| 239 | + time.sleep(1) |
| 240 | + |
| 241 | + # Generate the key pair |
| 242 | + public, private = session.generate_keypair(pkcs11.KeyType.RSA, key_size, public_template=public_key_template, |
| 243 | + private_template=private_key_template, |
| 244 | + label=PKCS11_KEY_LABEL) |
| 245 | + |
| 246 | + # Print the public key information |
| 247 | + key_info = format_key_output_rsa(public, "Public") |
| 248 | + print(key_info) |
| 249 | + |
| 250 | + # Print the private key information |
| 251 | + key_info = format_key_output_rsa(private, "Private") |
| 252 | + print(key_info) |
| 253 | + |
| 254 | + print("Key pair generated successfully! \n") |
| 255 | + time.sleep(1) |
| 256 | + |
| 257 | + try: |
| 258 | + # Verify if wrapping key exists. |
| 259 | + wrapping_key = session.get_key(label=PKCS11_WRAPPING_KEY_LABEL) |
| 260 | + if wrapping_key: |
| 261 | + print(f"Wrapping key with label: '{wrapping_key.label}' already exists. \n") |
| 262 | + time.sleep(1) |
| 263 | + print(f'Proceeding with wrap operation in 2 seconds...') |
| 264 | + time.sleep(1) |
| 265 | + print(f"Wrapping key label: '{PKCS11_KEY_LABEL}' key with wrapping key: '{PKCS11_WRAPPING_KEY_LABEL}'... ") |
| 266 | + time.sleep(2) |
| 267 | + |
| 268 | + # Wrap the RSA private key |
| 269 | + wrapped_key = wrapping_key.wrap_key(private, mechanism=pkcs11.Mechanism.AES_KEY_WRAP_PAD) # Required mech to wrap private RSA keys. |
| 270 | + |
| 271 | + except pkcs11.exceptions.NoSuchKey: |
| 272 | + # Generate the wrapping key if it doesn't exist |
| 273 | + print('Wrapping key does not exist. Generating a new wrapping key...') |
| 274 | + time.sleep(1) |
| 275 | + |
| 276 | + wrapping_key = session.generate_key(pkcs11.KeyType.AES, 256, label=PKCS11_WRAPPING_KEY_LABEL, store=True) # store=True is required otherwise it creates ephemeral keys which we do not want in a wrapping/unwrapping scenario. |
| 277 | + time.sleep(1) |
| 278 | + print('Wrapping key generated successfully!\n') |
| 279 | + |
| 280 | + # Print the wrapping key information |
| 281 | + key_info = format_key_output_aes(wrapping_key, "Wrapping") |
| 282 | + print(key_info) |
| 283 | + time.sleep(1) |
| 284 | + |
| 285 | + # Wrap the RSA private key |
| 286 | + wrapped_key = wrapping_key.wrap_key(private, mechanism=pkcs11.Mechanism.AES_KEY_WRAP_PAD) # Required mech to wrap private RSA keys. |
| 287 | + print('Wrapping RSA private key...\n') |
| 288 | + time.sleep(2) |
| 289 | + |
| 290 | + if wrapped_key: |
| 291 | + print(f"Private key label: '{PKCS11_KEY_LABEL}'successfully wrapped using wrapping key: '{PKCS11_WRAPPING_KEY_LABEL}'! \n") |
| 292 | + time.sleep(1) |
| 293 | + else: |
| 294 | + print('Error generating wrapping key') |
| 295 | + sys.exit(1) |
| 296 | + |
| 297 | + |
| 298 | +# Prompt to save the wrapped encrypted key material to a file |
| 299 | + |
| 300 | +save_to_file = get_user_input('Save the wrapped key material to a file? (y/n):', default='y').strip().lower() |
| 301 | + |
| 302 | +# Sanitize the PKCS11_KEY_LABEL |
| 303 | +sanitized_label = sanitize_filename(PKCS11_KEY_LABEL) |
| 304 | + |
| 305 | +# Get the current time for timestamping the filename |
| 306 | +current_time = time.strftime("%m_%d_%Y-%H_%M") |
| 307 | + |
| 308 | +if save_to_file == 'y': |
| 309 | + file_path = input("Enter the file name or full path to save the wrapped key (e.g /home/ or /opt/nfast/. Default is current working directory):").strip() |
| 310 | + |
| 311 | + # Default to the current working directory if no path is provided |
| 312 | + if not file_path: |
| 313 | + # Save the file in cwd with a timestamped filename |
| 314 | + file_path = os.path.join(os.getcwd(), f"{sanitized_label}_wrapped_key_{current_time}.bin") |
| 315 | + elif file_path: |
| 316 | + # Check if the provided path is a directory |
| 317 | + if os.path.isdir(file_path): |
| 318 | + file_path = os.path.join(file_path, f"{sanitized_label}_wrapped_key_{current_time}.bin") |
| 319 | + |
| 320 | + # Create any missing directories in the custom path |
| 321 | + dir_name = os.path.dirname(file_path) |
| 322 | + if dir_name and not os.path.exists(dir_name): |
| 323 | + os.makedirs(dir_name) |
| 324 | + |
| 325 | + # Write the wrapped key to the specified file |
| 326 | + with open(file_path, 'wb') as f: |
| 327 | + f.write(wrapped_key) |
| 328 | + print(f'Wrapped key saved to {file_path}') |
| 329 | + |
| 330 | + print("Private key wrapped successfully! \n") |
| 331 | + time.sleep(1) |
| 332 | +else: |
| 333 | + print("Wrapped key not saved.") |
| 334 | + |
| 335 | + |
| 336 | +# Generate a CSR using the public key |
| 337 | + |
| 338 | +# Prompt if you want to create a CSR |
| 339 | +csr_bool = input("Do you want to create a CSR using the public key? (y/n): ").strip().lower() |
| 340 | + |
| 341 | +if csr_bool != 'y': |
| 342 | + print('No CSR generated') |
| 343 | + sys.exit('Exiting...') |
| 344 | + |
| 345 | +elif csr_bool == 'y': |
| 346 | + |
| 347 | + key_label = input("Enter the key label (e.g., rsa_key): ").strip() |
| 348 | + appname_ident = extract_appname_ident(key_label) |
| 349 | + |
| 350 | + if appname_ident: |
| 351 | + print(f"Extracted identifier: {appname_ident} \n") |
| 352 | + else: |
| 353 | + sys.exit("Error: appname_ident not found \n") |
| 354 | + |
| 355 | + |
| 356 | + # Use the extracted appname_ident in the OpenSSL command |
| 357 | + command = f'/opt/nfast/openss/bin/openssl req -new -keyform engine -engine nfkm -key "{appname_ident}" -out {PKCS11_KEY_LABEL}_csr.pem' |
| 358 | + print("Running OpenSSL command... \n") |
| 359 | + |
| 360 | + try: |
| 361 | + output = subprocess.check_output(command, shell=True, text=True) |
| 362 | + print("CSR generated successfully! \n") |
| 363 | + print(output) |
| 364 | + except subprocess.CalledProcessError as e: |
| 365 | + print(f"Error generating CSR: {e}") |
| 366 | + sys.exit(1) |
| 367 | + |
| 368 | + # Prompt to save the CSR |
| 369 | +save_to_file = get_user_input('Save the CSR to a file? (y/n): ', default='y').strip().lower() |
| 370 | +if save_to_file == 'y': |
| 371 | + file_path = input("Enter the full path location to save the CSR (e.g /home/ or /opt/nfast/ default is current working directory): ").strip() |
| 372 | + |
| 373 | + # Default to the current working directory if no path is provided |
| 374 | + if not file_path: |
| 375 | + file_path = os.path.join(os.getcwd(), f"{sanitized_label}_csr.pem") |
| 376 | + else: |
| 377 | + # Check if the provided path is a directory |
| 378 | + if os.path.isdir(file_path): |
| 379 | + file_path = os.path.join(file_path, f"{sanitized_label}_csr.pem") |
| 380 | + |
| 381 | + # Create any missing directories in the custom path |
| 382 | + dir_name = os.path.dirname(file_path) |
| 383 | + if dir_name and not os.path.exists(dir_name): |
| 384 | + os.makedirs(dir_name) |
| 385 | + |
| 386 | + # Move the CSR file to the specified path |
| 387 | + os.rename(f'{PKCS11_KEY_LABEL}_csr.pem', file_path) |
| 388 | + print(f'CSR saved to {file_path}') |
| 389 | +else: |
| 390 | + print("CSR not saved. Exiting...") |
| 391 | + sys.exit(1) |
| 392 | + |
| 393 | + |
| 394 | +# If SAN is required, add the -reqexts SAN option to the command and provide the SAN details in the config file |
| 395 | +# (C:\Program Files\nCipher\nfast\openssl\openssl.cnf) |
| 396 | +# -reqexts SAN \-config <(cat /opt/nfast/openssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:example.com,DNS:www.example.com")) |
0 commit comments