diff --git a/README.md b/README.md index 004118a..bfdbf05 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,20 @@ you need to generate a workload for testing purposes. The project is in an early development stage and still a bit clumsy. -It requires **Python 3**. It was tested on **Python 3.7**, **3.8** and **3.9** using a **payShield 10k** with firmware **1.3a** +It requires **Python 3**. It was tested on **Python 3.7**, **3.8** and **3.9** using a **payShield 10k** with firmware **1.3d** ## Version -**1.1.4a** +**1.1.5** ## Usage - pressureTest.py [-h] [--port PORT] [--key {2048,4096} | --nc | --no | --pci | --j2 | --j4 | --j8 | --jk | --randgen | --b2] - [--header HEADER] [--times TIMES] [--forever] [--decode] [--proto {tcp,udp,tls}] - [--keyfile KEYFILE] [--crtfile CRTFILE] [--echo] - host + pressureTest.py [-h] [--port PORT] + [--key KEY | --nc | --no | --pci | --j2 | --j4 | --j8 | --jk | --b2 | --randgen | --ecc] + [--ecc-curve {0,1,2}] [--key-use {S,X,N}] [--key-exportability {N,E,S}] [--header HEADER] + [--forever] [--decode] [--times TIMES] [--proto {tcp,udp,tls}] [--keyfile KEYFILE] + [--crtfile CRTFILE] [--echo ECHO] + host ### Mandatory parameter(s) @@ -33,35 +35,38 @@ It requires **Python 3**. It was tested on **Python 3.7**, **3.8** and **3.9** u ### Mutually exclusive parameters -**--key** the length of the RSA to generate. There are only two valid values: **2048** or **4096**. -If not specified, **2048** is the default. +**--key** the length of the RSA to generate. There value need to be between **320** and **4096**. **--nc** performs just an **NC** test. **--pci** gathers the PCI compliance status of the payShield through the **NO** command, type **01**. -**--no** gathers the status of the payShield through the **NO** command. +**--no** gathers the status of the payShield through the **NO** command, type **00**. -**--j2** get HSM Loading using **J2** command. +**--j2** gets HSM Loading using **J2** command. -**--j4** get Host Command Volumes using **J4** command. +**--j4** gets Host Command Volumes using **J4** command. -**--j8** get Health Check Accumulated Counts using **J8** command. +**--j8** gets Health Check Accumulated Counts using **J8** command. -**--jk** get Instantaneous Health Check Status using **JK** command. +**--jk** gets Instantaneous Health Check Status using **JK** command. -**--randgen** Generate a random value 8 bytes long using **N0** command. +**--randgen** Generates a random value 8 bytes long using **N0** command. -**--b2** Echo received data, specified through the **--echo** parameter, back to the user. +**--b2** Echoes received data, specified through the **--echo** parameter, back to the user. + +**--ecc** Generates an ECC public/private key pair using the Elliptic Curve algorithm. +By default, the curve used is curve used is NIST P-521, the exportability is 'S' (Sensitive) +and the key usage is 'S' (Only digital signature). +Use the parameters **--ecc-curve**, **--key-use** and **--key-exportability** to change the default values. ### Optional parameters **--port** specifies the host port, if omitted the default value **1500** is used. -**--proto** specify the protocol to use, **tcp**, **udp** or **tls**, if omitted the default value **tcp** +**--proto** specifies the protocol to use, **tcp**, **udp** or **tls**, if omitted the default value **tcp** is used. -If **tls** is used you might specify the path of the client key file and the certificate using the parameters ** ---keyfile** and **--crtfile**. +If **tls** is used you might specify the path of the client key file and the certificate using the parameters **--keyfile** and **--crtfile**. **--keyfile** the path of the client key file, if is not specified the default value is **client.key**. It's only considered if the protocol is **tls**. @@ -71,20 +76,38 @@ It's only considered if the protocol is **tls**. **--header** the header string to prefix to the host command, if not specified the default value is **HEAD**. -**--echo** specify the payload sent using the echo command **--b2**, otherwise it is ignored +**--echo** specifies the payload sent using the echo command **--b2**, otherwise it is ignored **--forever** the test will run forever. Use **CTRL-C** to terminate it. **--times** how many times execute the test. If it is not specified the default value is **1000** times. **--decode** decodes the response of the payShield if a decoder function is available for the command. -The commands **--decode** supports in the release are: **B2**, **N0**, **NO**, **NC**, **J2**, **J4**, **J8** and **JK**. - +The commands **--decode** supports in the release are: **B2**, **N0**, **NO**, **NC**, **J2**, **J4**, **J8**, **JK** and **FY (ECC)**. + +**--ecc-curve** sets the ECC curve to use when **--ecc** is used. The default is NIST P-521. +The possible choices are: + - 0: FIPS 186-3 – NIST P-256 + - 1: FIPS 186-3 – NIST P-384 + - 2: FIPS 186-3 – NIST P-521 + +**--key-use** sets the key usage. The default one is **'S'** (Signature only). +The possible choices are: + - S: The key may only be used to perform digital signature generation operations. + - X: The key may only be used to derive other keys. + - N: No special restrictions apply. + +**--key-exportability** sets the key exportability. The default is **'S'** (Sensitive). +The possible choices are: + - E: May only be exported in a trusted key block, provided the wrapping key itself is in a trusted format. + - N: No export permitted. + - S: Sensitive; all other export possibilities are permitted, provided such export has been enabled (existing Authorized State requirements remain). + ## Example C:\Test>python pressureTest.py 192.168.0.36 --nc --times 2 - PayShield stress utility, version 1.1.3, by Marco S. Zuppone - msz@msz.eu - https://msz.eu + PayShield stress utility, version 1.1.5, by Marco S. Zuppone - msz@msz.eu - https://msz.eu To get more info about the usage invoke it with the -h option This software is open source, and it is under the Affero AGPL 3.0 license @@ -112,6 +135,8 @@ The commands **--decode** supports in the release are: **B2**, **N0**, **NO**, * The **EI** command used to generate the RSA key requires authorization, and the generation of 4096-bit keys is possible only for keyblock LMKs. +The **--ecc** parameter uses the **FY** command to generate ECC keypairs: +Depending on the firmware version the functionality may require a license and/or a firmware update. ## COPYRIGHT & LICENSE Please refer to the **LICENSE** file that is part of this project. @@ -130,4 +155,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.** See the **GNU Affero General Public License** for more details. ## Questions, bugs & suggestions -For any questions, feedback, suggestions, send money ***(yes...it's a dream I know)*** you can contact the author at **msz@msz.eu** +For any questions, feedback, suggestions, send money ***(yes...it's a dream, I know)*** you can contact the author at [msz@msz.eu](mailto:msz@msz.eu) diff --git a/pressureTest.py b/pressureTest.py index 9c00da8..8e941d8 100644 --- a/pressureTest.py +++ b/pressureTest.py @@ -13,7 +13,7 @@ from typing import Tuple, Dict from types import FunctionType -VERSION = "1.1.4a" +VERSION = "1.1.5" def decode_n0(response_to_decode: bytes, head_len: int): @@ -162,74 +162,6 @@ def decode_j8(response_to_decode: bytes, head_len: int): print("Pin attacks: ", response_to_decode[str_pointer:str_pointer + 8]) -def decode_nc(response_to_decode: bytes, head_len: int): - """ - It decodes the result of the command NC an prints the meaning of the returned output - The message trailer is not considered - - Parameters - ___________ - response_to_decode: bytes - The response returned by the payShield - head_len: int - The length of the header - - Returns - ___________ - nothing - """ - response_to_decode, msg_len, str_pointer = common_parser(response_to_decode, head_len) - if response_to_decode[str_pointer:str_pointer + 2] == '00': - str_pointer = str_pointer + 2 - print("LMK CRC:", response_to_decode[str_pointer:str_pointer + 16]) - str_pointer = str_pointer + 16 - print("Firmware number:", response_to_decode[str_pointer:str_pointer + 9]) - - -def decode_j8(response_to_decode: bytes, head_len: int): - """ - It decodes the result of the command J8 an prints the meaning of the returned output - The message trailer is not considered - - Parameters - ___________ - response_to_decode: bytes - The response returned by the payShield - head_len: int - The length of the header - - Returns - ___________ - nothing - """ - response_to_decode, msg_len, str_pointer = common_parser(response_to_decode, head_len) - if response_to_decode[str_pointer:str_pointer + 2] == '00': - str_pointer = str_pointer + 2 - print("Serial Number: ", response_to_decode[str_pointer:str_pointer + 12]) - str_pointer = str_pointer + 12 - print("Start Date: ", response_to_decode[str_pointer:str_pointer + 6]) - str_pointer = str_pointer + 6 - print("Start Time: ", response_to_decode[str_pointer:str_pointer + 6]) - str_pointer = str_pointer + 6 - print("End Date: ", response_to_decode[str_pointer:str_pointer + 6]) - str_pointer = str_pointer + 6 - print("End Time: ", response_to_decode[str_pointer:str_pointer + 6]) - str_pointer = str_pointer + 6 - print("Current Date: ", response_to_decode[str_pointer:str_pointer + 6]) - str_pointer = str_pointer + 6 - print("Current Time: ", response_to_decode[str_pointer:str_pointer + 6]) - str_pointer = str_pointer + 6 - print("Reboots: ", response_to_decode[str_pointer:str_pointer + 10]) - str_pointer = str_pointer + 10 - print("Tampers: ", response_to_decode[str_pointer:str_pointer + 10]) - str_pointer = str_pointer + 10 - print("Pin verifies/minute: ", response_to_decode[str_pointer:str_pointer + 7]) - str_pointer = str_pointer + 7 - print("Pin verifies/hour: ", response_to_decode[str_pointer:str_pointer + 5]) - str_pointer = str_pointer + 5 - print("Pin attacks: ", response_to_decode[str_pointer:str_pointer + 8]) - - def decode_b2(response_to_decode: bytes, head_len: int): """ It decodes the result of the command B2 an prints the meaning of the returned output @@ -360,7 +292,7 @@ def decode_jk(response_to_decode: bytes, head_len: int): nothing """ # structures to decode the result - # We cam use CONSOLE_STATUS_CODE to check the status of the payShield Manager as well. + # We can use CONSOLE_STATUS_CODE to check the status of the payShield Manager as well. CONSOLE_STATUS_CODE = { '0': 'unknown', @@ -487,6 +419,36 @@ def decode_jk(response_to_decode: bytes, head_len: int): print("") +def decode_ecc(response_to_decode: bytes, head_len: int): + """ + It decodes the result of the command FY an prints the meaning of the returned output + + Parameters + ___________ + response_to_decode: bytes + The response returned by the payShield + head_len: int + The length of the header + + Returns + ___________ + nothing + """ + response_to_decode, msg_len, str_pointer = common_parser(response_to_decode, head_len) + if response_to_decode[str_pointer:str_pointer + 2] == '00': + str_pointer = str_pointer + 2 + key_len = int(response_to_decode[str_pointer:str_pointer + 4]) + print("ECC Public Key Length: ", key_len) + str_pointer = str_pointer + 4 + print("ECC Public Key", + binascii.hexlify((response_to_decode[str_pointer:str_pointer + key_len]).encode()) + .decode('ascii', 'ignore')) + print("Public/private separator: ", response_to_decode[str_pointer + key_len:str_pointer + key_len + 1]) + str_pointer = str_pointer + key_len + 1 + print("ECC Private Key under LMK", + response_to_decode[str_pointer:]) + + def payshield_error_codes(error_code: str) -> str: """This function maps the result code with the error message. I derived the list of errors and messages from the following manual: @@ -595,7 +557,11 @@ def payshield_error_codes(error_code: str) -> str: 'BB': 'Invalid wrapping key', 'BC': 'Repeated optional block', 'BD': 'Incompatible key types', - 'BE': 'Invalid key block header ID'} + 'BE': 'Invalid key block header ID', + 'D2': 'Invalid curve reference', + 'D3': 'Invalid Key Encoding', + 'E0': 'Invalid command version number' + } return PAYSHIELD_ERROR_CODE.get(error_code, "Unknown error") @@ -827,7 +793,8 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i 'J2': decode_j2, 'J4': decode_j4, 'JK': decode_jk, - 'B2': decode_b2 + 'B2': decode_b2, + 'FY': decode_ecc } parser = argparse.ArgumentParser( @@ -837,8 +804,7 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i parser.add_argument("host", help="Ip address or hostname of the payShield") group = parser.add_mutually_exclusive_group() parser.add_argument("--port", "-p", help="The host port", default=1500, type=int) - group.add_argument("--key", help="RSA key length. Accepted values are 2048 and 4096.", - default=2048, choices=[2048, 4096], type=int) + group.add_argument("--key", help="RSA key length. Accepted values are between 320 and 4096.", type=int) group.add_argument("--nc", help="Just perform a NC test. ", action="store_true") group.add_argument("--no", help="Retrieves HSM status information using NO command. ", @@ -860,6 +826,15 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i help="Echo received data back to the user.", action="store_true") group.add_argument("--randgen", help="Generate a random value 8 bytes long.", action="store_true") + group.add_argument("--ecc", + help="Generate an ECC public/private key pair using the Elliptic Curve algorithm curve NIST " + "P-521.", + action="store_true") + parser.add_argument("--ecc-curve", help="select the ECC curve.", default='0', type=str, choices=['0', '1', '2']) + parser.add_argument("--key-use", help="select the key mode of use.", default='S', type=str.upper, + choices=['S', 'X', 'N']) + parser.add_argument("--key-exportability", help="select the key exportability.", default='S', type=str.upper, + choices=['N', 'E', 'S']) parser.add_argument("--header", help="the header string to prepend to the host command. If not specified the default is HEAD.", default="HEAD", type=str) @@ -881,10 +856,16 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i args = parser.parse_args() # the order of the IF here is important due to the default arguments. # All the mutually exclusive options need to be in this block where ELIF statements are used. - if args.key == 2048: - command = args.header + 'EI2204801#0000' - elif args.key == 4096: - command = args.header + 'EI2409601#0000' + command = '' + if args.key is not None: + if 320 <= args.key <= 4096: + k_len_str = str(args.key) + if len(k_len_str) <= 3: + k_len_str = '0' + k_len_str + command = args.header + 'EI2' + k_len_str + '01#0000' + elif args.key < 320 or args.key > 4096: + print("The key length value needs to be between 320 and 4096") + exit() elif args.nc: command = args.header + 'NC' elif args.no: @@ -901,6 +882,8 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i command = args.header + 'JK' elif args.randgen: command = args.header + 'N0008' + elif args.ecc: + command = args.header + 'FY010' + args.ecc_curve + '03#' + args.key_use + '00' + args.key_exportability + '00' if args.b2: # we need to calculate the hexadecimal representation of the length of the payload string # the length of the string field is 4 char long so we need to format it accordingly @@ -910,8 +893,17 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i h_padding = '0000' len_echo_message = len(args.echo) hex_string_len = hex(len_echo_message).lstrip('0x').upper() + # using lstrip() to strip the '0x' prefix is acceptable due to the expected pattern + # Ideally you should use removeprefix() but it was introduced in python 3.9 and I want to keep compatibility hex_string_len = h_padding[:4 - len(hex_string_len)] + hex_string_len command = args.header + 'B2' + hex_string_len + args.echo + + # IMPORTANT: At this point the 'command' need to contain something. + # If you want to add to the tool command link arguments about commands do it before this comment block + # Now we verify if the command variable is empty. In this case we thrown an error. + if len(command) == 0: + print("You forgot to specify the action you want to to perform on the payShield") + exit() if args.proto == 'tls': # check that the cert and key files are accessible if not (args.keyfile.exists() and args.crtfile.exists()):