Skip to content

Commit 074e11c

Browse files
authored
Merge pull request #17 from mszeu/devel
ECC key generation and decode
2 parents 7cb9340 + 3d3f324 commit 074e11c

File tree

2 files changed

+118
-101
lines changed

2 files changed

+118
-101
lines changed

README.md

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,54 +14,59 @@ you need to generate a workload for testing purposes.
1414

1515
The project is in an early development stage and still a bit clumsy.
1616

17-
It requires **Python 3**. It was tested on **Python 3.7**, **3.8** and **3.9** using a **payShield 10k** with firmware **1.3a**
17+
It requires **Python 3**. It was tested on **Python 3.7**, **3.8** and **3.9** using a **payShield 10k** with firmware **1.3d**
1818

1919
## Version
2020

21-
**1.1.4a**
21+
**1.1.5**
2222

2323
## Usage
2424

25-
pressureTest.py [-h] [--port PORT] [--key {2048,4096} | --nc | --no | --pci | --j2 | --j4 | --j8 | --jk | --randgen | --b2]
26-
[--header HEADER] [--times TIMES] [--forever] [--decode] [--proto {tcp,udp,tls}]
27-
[--keyfile KEYFILE] [--crtfile CRTFILE] [--echo]
28-
host
25+
pressureTest.py [-h] [--port PORT]
26+
[--key KEY | --nc | --no | --pci | --j2 | --j4 | --j8 | --jk | --b2 | --randgen | --ecc]
27+
[--ecc-curve {0,1,2}] [--key-use {S,X,N}] [--key-exportability {N,E,S}] [--header HEADER]
28+
[--forever] [--decode] [--times TIMES] [--proto {tcp,udp,tls}] [--keyfile KEYFILE]
29+
[--crtfile CRTFILE] [--echo ECHO]
30+
host
2931

3032
### Mandatory parameter(s)
3133

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

3436
### Mutually exclusive parameters
3537

36-
**--key** the length of the RSA to generate. There are only two valid values: **2048** or **4096**.
37-
If not specified, **2048** is the default.
38+
**--key** the length of the RSA to generate. There value need to be between **320** and **4096**.
3839

3940
**--nc** performs just an **NC** test.
4041

4142
**--pci** gathers the PCI compliance status of the payShield through the **NO** command, type **01**.
4243

43-
**--no** gathers the status of the payShield through the **NO** command.
44+
**--no** gathers the status of the payShield through the **NO** command, type **00**.
4445

45-
**--j2** get HSM Loading using **J2** command.
46+
**--j2** gets HSM Loading using **J2** command.
4647

47-
**--j4** get Host Command Volumes using **J4** command.
48+
**--j4** gets Host Command Volumes using **J4** command.
4849

49-
**--j8** get Health Check Accumulated Counts using **J8** command.
50+
**--j8** gets Health Check Accumulated Counts using **J8** command.
5051

51-
**--jk** get Instantaneous Health Check Status using **JK** command.
52+
**--jk** gets Instantaneous Health Check Status using **JK** command.
5253

53-
**--randgen** Generate a random value 8 bytes long using **N0** command.
54+
**--randgen** Generates a random value 8 bytes long using **N0** command.
5455

55-
**--b2** Echo received data, specified through the **--echo** parameter, back to the user.
56+
**--b2** Echoes received data, specified through the **--echo** parameter, back to the user.
57+
58+
**--ecc** Generates an ECC public/private key pair using the Elliptic Curve algorithm.
59+
By default, the curve used is curve used is NIST P-521, the exportability is 'S' (Sensitive)
60+
and the key usage is 'S' (Only digital signature).
61+
Use the parameters **--ecc-curve**, **--key-use** and **--key-exportability** to change the default values.
5662

5763
### Optional parameters
5864

5965
**--port** specifies the host port, if omitted the default value **1500** is used.
6066

61-
**--proto** specify the protocol to use, **tcp**, **udp** or **tls**, if omitted the default value **tcp**
67+
**--proto** specifies the protocol to use, **tcp**, **udp** or **tls**, if omitted the default value **tcp**
6268
is used.
63-
If **tls** is used you might specify the path of the client key file and the certificate using the parameters **
64-
--keyfile** and **--crtfile**.
69+
If **tls** is used you might specify the path of the client key file and the certificate using the parameters **--keyfile** and **--crtfile**.
6570

6671
**--keyfile** the path of the client key file, if is not specified the default value is **client.key**.
6772
It's only considered if the protocol is **tls**.
@@ -71,20 +76,38 @@ It's only considered if the protocol is **tls**.
7176

7277
**--header** the header string to prefix to the host command, if not specified the default value is **HEAD**.
7378

74-
**--echo** specify the payload sent using the echo command **--b2**, otherwise it is ignored
79+
**--echo** specifies the payload sent using the echo command **--b2**, otherwise it is ignored
7580

7681
**--forever** the test will run forever. Use **CTRL-C** to terminate it.
7782

7883
**--times** how many times execute the test. If it is not specified the default value is **1000** times.
7984

8085
**--decode** decodes the response of the payShield if a decoder function is available for the command.
81-
The commands **--decode** supports in the release are: **B2**, **N0**, **NO**, **NC**, **J2**, **J4**, **J8** and **JK**.
82-
86+
The commands **--decode** supports in the release are: **B2**, **N0**, **NO**, **NC**, **J2**, **J4**, **J8**, **JK** and **FY (ECC)**.
87+
88+
**--ecc-curve** sets the ECC curve to use when **--ecc** is used. The default is NIST P-521.
89+
The possible choices are:
90+
- 0: FIPS 186-3 – NIST P-256
91+
- 1: FIPS 186-3 – NIST P-384
92+
- 2: FIPS 186-3 – NIST P-521
93+
94+
**--key-use** sets the key usage. The default one is **'S'** (Signature only).
95+
The possible choices are:
96+
- S: The key may only be used to perform digital signature generation operations.
97+
- X: The key may only be used to derive other keys.
98+
- N: No special restrictions apply.
99+
100+
**--key-exportability** sets the key exportability. The default is **'S'** (Sensitive).
101+
The possible choices are:
102+
- E: May only be exported in a trusted key block, provided the wrapping key itself is in a trusted format.
103+
- N: No export permitted.
104+
- S: Sensitive; all other export possibilities are permitted, provided such export has been enabled (existing Authorized State requirements remain).
105+
83106
## Example
84107

85108
C:\Test>python pressureTest.py 192.168.0.36 --nc --times 2
86109

87-
PayShield stress utility, version 1.1.3, by Marco S. Zuppone - msz@msz.eu - https://msz.eu
110+
PayShield stress utility, version 1.1.5, by Marco S. Zuppone - msz@msz.eu - https://msz.eu
88111
To get more info about the usage invoke it with the -h option This software is open source, and it is under the Affero
89112
AGPL 3.0 license
90113

@@ -112,6 +135,8 @@ The commands **--decode** supports in the release are: **B2**, **N0**, **NO**, *
112135

113136
The **EI** command used to generate the RSA key requires authorization, and the generation of 4096-bit keys is possible only for keyblock LMKs.
114137

138+
The **--ecc** parameter uses the **FY** command to generate ECC keypairs:
139+
Depending on the firmware version the functionality may require a license and/or a firmware update.
115140

116141
## COPYRIGHT & LICENSE
117142
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
130155
**GNU Affero General Public License** for more details.
131156

132157
## Questions, bugs & suggestions
133-
For any questions, feedback, suggestions, send money ***(yes...it's a dream I know)*** you can contact the author at **msz@msz.eu**
158+
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)

pressureTest.py

Lines changed: 70 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from typing import Tuple, Dict
1414
from types import FunctionType
1515

16-
VERSION = "1.1.4a"
16+
VERSION = "1.1.5"
1717

1818

1919
def decode_n0(response_to_decode: bytes, head_len: int):
@@ -162,74 +162,6 @@ def decode_j8(response_to_decode: bytes, head_len: int):
162162
print("Pin attacks: ", response_to_decode[str_pointer:str_pointer + 8])
163163

164164

165-
def decode_nc(response_to_decode: bytes, head_len: int):
166-
"""
167-
It decodes the result of the command NC an prints the meaning of the returned output
168-
The message trailer is not considered
169-
170-
Parameters
171-
___________
172-
response_to_decode: bytes
173-
The response returned by the payShield
174-
head_len: int
175-
The length of the header
176-
177-
Returns
178-
___________
179-
nothing
180-
"""
181-
response_to_decode, msg_len, str_pointer = common_parser(response_to_decode, head_len)
182-
if response_to_decode[str_pointer:str_pointer + 2] == '00':
183-
str_pointer = str_pointer + 2
184-
print("LMK CRC:", response_to_decode[str_pointer:str_pointer + 16])
185-
str_pointer = str_pointer + 16
186-
print("Firmware number:", response_to_decode[str_pointer:str_pointer + 9])
187-
188-
189-
def decode_j8(response_to_decode: bytes, head_len: int):
190-
"""
191-
It decodes the result of the command J8 an prints the meaning of the returned output
192-
The message trailer is not considered
193-
194-
Parameters
195-
___________
196-
response_to_decode: bytes
197-
The response returned by the payShield
198-
head_len: int
199-
The length of the header
200-
201-
Returns
202-
___________
203-
nothing
204-
"""
205-
response_to_decode, msg_len, str_pointer = common_parser(response_to_decode, head_len)
206-
if response_to_decode[str_pointer:str_pointer + 2] == '00':
207-
str_pointer = str_pointer + 2
208-
print("Serial Number: ", response_to_decode[str_pointer:str_pointer + 12])
209-
str_pointer = str_pointer + 12
210-
print("Start Date: ", response_to_decode[str_pointer:str_pointer + 6])
211-
str_pointer = str_pointer + 6
212-
print("Start Time: ", response_to_decode[str_pointer:str_pointer + 6])
213-
str_pointer = str_pointer + 6
214-
print("End Date: ", response_to_decode[str_pointer:str_pointer + 6])
215-
str_pointer = str_pointer + 6
216-
print("End Time: ", response_to_decode[str_pointer:str_pointer + 6])
217-
str_pointer = str_pointer + 6
218-
print("Current Date: ", response_to_decode[str_pointer:str_pointer + 6])
219-
str_pointer = str_pointer + 6
220-
print("Current Time: ", response_to_decode[str_pointer:str_pointer + 6])
221-
str_pointer = str_pointer + 6
222-
print("Reboots: ", response_to_decode[str_pointer:str_pointer + 10])
223-
str_pointer = str_pointer + 10
224-
print("Tampers: ", response_to_decode[str_pointer:str_pointer + 10])
225-
str_pointer = str_pointer + 10
226-
print("Pin verifies/minute: ", response_to_decode[str_pointer:str_pointer + 7])
227-
str_pointer = str_pointer + 7
228-
print("Pin verifies/hour: ", response_to_decode[str_pointer:str_pointer + 5])
229-
str_pointer = str_pointer + 5
230-
print("Pin attacks: ", response_to_decode[str_pointer:str_pointer + 8])
231-
232-
233165
def decode_b2(response_to_decode: bytes, head_len: int):
234166
"""
235167
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):
360292
nothing
361293
"""
362294
# structures to decode the result
363-
# We cam use CONSOLE_STATUS_CODE to check the status of the payShield Manager as well.
295+
# We can use CONSOLE_STATUS_CODE to check the status of the payShield Manager as well.
364296

365297
CONSOLE_STATUS_CODE = {
366298
'0': 'unknown',
@@ -487,6 +419,36 @@ def decode_jk(response_to_decode: bytes, head_len: int):
487419
print("")
488420

489421

422+
def decode_ecc(response_to_decode: bytes, head_len: int):
423+
"""
424+
It decodes the result of the command FY an prints the meaning of the returned output
425+
426+
Parameters
427+
___________
428+
response_to_decode: bytes
429+
The response returned by the payShield
430+
head_len: int
431+
The length of the header
432+
433+
Returns
434+
___________
435+
nothing
436+
"""
437+
response_to_decode, msg_len, str_pointer = common_parser(response_to_decode, head_len)
438+
if response_to_decode[str_pointer:str_pointer + 2] == '00':
439+
str_pointer = str_pointer + 2
440+
key_len = int(response_to_decode[str_pointer:str_pointer + 4])
441+
print("ECC Public Key Length: ", key_len)
442+
str_pointer = str_pointer + 4
443+
print("ECC Public Key",
444+
binascii.hexlify((response_to_decode[str_pointer:str_pointer + key_len]).encode())
445+
.decode('ascii', 'ignore'))
446+
print("Public/private separator: ", response_to_decode[str_pointer + key_len:str_pointer + key_len + 1])
447+
str_pointer = str_pointer + key_len + 1
448+
print("ECC Private Key under LMK",
449+
response_to_decode[str_pointer:])
450+
451+
490452
def payshield_error_codes(error_code: str) -> str:
491453
"""This function maps the result code with the error message.
492454
I derived the list of errors and messages from the following manual:
@@ -595,7 +557,11 @@ def payshield_error_codes(error_code: str) -> str:
595557
'BB': 'Invalid wrapping key',
596558
'BC': 'Repeated optional block',
597559
'BD': 'Incompatible key types',
598-
'BE': 'Invalid key block header ID'}
560+
'BE': 'Invalid key block header ID',
561+
'D2': 'Invalid curve reference',
562+
'D3': 'Invalid Key Encoding',
563+
'E0': 'Invalid command version number'
564+
}
599565

600566
return PAYSHIELD_ERROR_CODE.get(error_code, "Unknown error")
601567

@@ -827,7 +793,8 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i
827793
'J2': decode_j2,
828794
'J4': decode_j4,
829795
'JK': decode_jk,
830-
'B2': decode_b2
796+
'B2': decode_b2,
797+
'FY': decode_ecc
831798
}
832799

833800
parser = argparse.ArgumentParser(
@@ -837,8 +804,7 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i
837804
parser.add_argument("host", help="Ip address or hostname of the payShield")
838805
group = parser.add_mutually_exclusive_group()
839806
parser.add_argument("--port", "-p", help="The host port", default=1500, type=int)
840-
group.add_argument("--key", help="RSA key length. Accepted values are 2048 and 4096.",
841-
default=2048, choices=[2048, 4096], type=int)
807+
group.add_argument("--key", help="RSA key length. Accepted values are between 320 and 4096.", type=int)
842808
group.add_argument("--nc", help="Just perform a NC test. ",
843809
action="store_true")
844810
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
860826
help="Echo received data back to the user.", action="store_true")
861827
group.add_argument("--randgen",
862828
help="Generate a random value 8 bytes long.", action="store_true")
829+
group.add_argument("--ecc",
830+
help="Generate an ECC public/private key pair using the Elliptic Curve algorithm curve NIST "
831+
"P-521.",
832+
action="store_true")
833+
parser.add_argument("--ecc-curve", help="select the ECC curve.", default='0', type=str, choices=['0', '1', '2'])
834+
parser.add_argument("--key-use", help="select the key mode of use.", default='S', type=str.upper,
835+
choices=['S', 'X', 'N'])
836+
parser.add_argument("--key-exportability", help="select the key exportability.", default='S', type=str.upper,
837+
choices=['N', 'E', 'S'])
863838
parser.add_argument("--header",
864839
help="the header string to prepend to the host command. If not specified the default is HEAD.",
865840
default="HEAD", type=str)
@@ -881,10 +856,16 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i
881856
args = parser.parse_args()
882857
# the order of the IF here is important due to the default arguments.
883858
# All the mutually exclusive options need to be in this block where ELIF statements are used.
884-
if args.key == 2048:
885-
command = args.header + 'EI2204801#0000'
886-
elif args.key == 4096:
887-
command = args.header + 'EI2409601#0000'
859+
command = ''
860+
if args.key is not None:
861+
if 320 <= args.key <= 4096:
862+
k_len_str = str(args.key)
863+
if len(k_len_str) <= 3:
864+
k_len_str = '0' + k_len_str
865+
command = args.header + 'EI2' + k_len_str + '01#0000'
866+
elif args.key < 320 or args.key > 4096:
867+
print("The key length value needs to be between 320 and 4096")
868+
exit()
888869
elif args.nc:
889870
command = args.header + 'NC'
890871
elif args.no:
@@ -901,6 +882,8 @@ def common_parser(response_to_decode: bytes, head_len: int) -> Tuple[str, int, i
901882
command = args.header + 'JK'
902883
elif args.randgen:
903884
command = args.header + 'N0008'
885+
elif args.ecc:
886+
command = args.header + 'FY010' + args.ecc_curve + '03#' + args.key_use + '00' + args.key_exportability + '00'
904887
if args.b2:
905888
# we need to calculate the hexadecimal representation of the length of the payload string
906889
# 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
910893
h_padding = '0000'
911894
len_echo_message = len(args.echo)
912895
hex_string_len = hex(len_echo_message).lstrip('0x').upper()
896+
# using lstrip() to strip the '0x' prefix is acceptable due to the expected pattern
897+
# Ideally you should use removeprefix() but it was introduced in python 3.9 and I want to keep compatibility
913898
hex_string_len = h_padding[:4 - len(hex_string_len)] + hex_string_len
914899
command = args.header + 'B2' + hex_string_len + args.echo
900+
901+
# IMPORTANT: At this point the 'command' need to contain something.
902+
# If you want to add to the tool command link arguments about commands do it before this comment block
903+
# Now we verify if the command variable is empty. In this case we thrown an error.
904+
if len(command) == 0:
905+
print("You forgot to specify the action you want to to perform on the payShield")
906+
exit()
915907
if args.proto == 'tls':
916908
# check that the cert and key files are accessible
917909
if not (args.keyfile.exists() and args.crtfile.exists()):

0 commit comments

Comments
 (0)