Skip to content

Commit ea1ec61

Browse files
authored
Script for Windows and Linux using HSM and PCKS11
Creates or uses an exsisting an RSA Key Pair on an nShield HSM device Wraps the RSA private key with an exisiting or newly generated AES wrapping key. File is exported in encrypted binary format. Generates a CSR using the RSA key stored in HSM via OpenSSL with nfkm engine.
1 parent bc2cc6e commit ea1ec61

File tree

2 files changed

+796
-0
lines changed

2 files changed

+796
-0
lines changed

rsa-gen-csr-aes-wrap-linux.py

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
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

Comments
 (0)