Skip to content

Commit

Permalink
Merge pull request #17 from mszeu/devel
Browse files Browse the repository at this point in the history
ECC key generation and decode
  • Loading branch information
mszeu authored Oct 7, 2021
2 parents 7cb9340 + 3d3f324 commit 074e11c
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 101 deletions.
71 changes: 48 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,54 +14,59 @@ 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)

**host** *ip address* or the *hostname/fqdn* of the **payShield** appliance.

### 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**.
Expand All @@ -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 - [email protected] - https://msz.eu
PayShield stress utility, version 1.1.5, by Marco S. Zuppone - [email protected] - 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

Expand Down Expand Up @@ -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.
Expand All @@ -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 **[email protected]**
For any questions, feedback, suggestions, send money ***(yes...it's a dream, I know)*** you can contact the author at [[email protected]](mailto:[email protected])
148 changes: 70 additions & 78 deletions pressureTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")

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

0 comments on commit 074e11c

Please sign in to comment.