Add first battery of tests.

It contains keypair generation for ECC and RSA, PIN blocking and signature generation and verification.

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos 2023-01-17 13:41:10 +01:00
parent 8976dc1f79
commit 1af461c206
No known key found for this signature in database
GPG key ID: C0095B7870A4CCD3
5 changed files with 511 additions and 0 deletions

246
tests/conftest.py Normal file
View file

@ -0,0 +1,246 @@
import sys
import pytest
from binascii import hexlify
from utils import APDUResponse, DOPrefixes, KeyType, Algorithm
import hashlib
try:
from cvc.asn1 import ASN1
from cvc import oid
from cvc.certificates import CVC
from cvc.ec_curves import ec_domain, find_curve
except ModuleNotFoundError:
print('ERROR: cvc module not found! Install pycvc package.\nTry with `pip install pycvc`')
sys.exit(-1)
try:
from smartcard.CardType import AnyCardType
from smartcard.CardRequest import CardRequest
from smartcard.Exceptions import CardRequestTimeoutException, CardConnectionException
except ModuleNotFoundError:
print('ERROR: smarctard module not found! Install pyscard package.\nTry with `pip install pyscard`')
sys.exit(-1)
try:
from cryptography.hazmat.primitives.asymmetric import ec, rsa, utils, padding
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
from cryptography.hazmat.primitives import hashes, serialization
except ModuleNotFoundError:
print('ERROR: cryptography module not found! Install cryptography package.\nTry with `pip install cryptography`')
sys.exit(-1)
class Device:
class EcDummy:
def __init__(self, name):
self.name = name
def __init__(self,pin='648219'):
self.__pin = pin
cardtype = AnyCardType()
try:
# request card insertion
cardrequest = CardRequest(timeout=10, cardType=cardtype)
self.__card = cardrequest.waitforcard()
# connect to the card and perform a few transmits
self.__card.connection.connect()
except CardRequestTimeoutException:
raise Exception('time-out: no card inserted during last 10s')
self.select_applet()
def select_applet(self):
self.__card.connection.transmit([0x00, 0xA4, 0x04, 0x00, 0xB, 0xE8, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xC3, 0x1F, 0x02, 0x01, 0x0])
def send(self, command, cla=0x00, p1=0x00, p2=0x00, ne=None, data=None):
lc = []
dataf = []
if (data):
lc = [0x00] + list(len(data).to_bytes(2, 'big'))
dataf = list(data)
if (ne is None):
le = [0x00, 0x00]
else:
le = list(ne.to_bytes(2, 'big'))
if (isinstance(command, list) and len(command) > 1):
apdu = command
else:
apdu = [cla, command]
apdu = apdu + [p1, p2] + lc + dataf + le
try:
response, sw1, sw2 = self.__card.connection.transmit(apdu)
except CardConnectionException:
self.__card.connection.reconnect()
response, sw1, sw2 = self.__card.connection.transmit(apdu)
if (sw1 != 0x90):
if (sw1 == 0x63 and sw2 & 0xF0 == 0xC0):
pass
elif (sw1 == 0x6A and sw2 == 0x82):
self.select_applet()
if (sw1 == 0x90):
response, sw1, sw2 = self.__card.connection.transmit(apdu)
if (sw1 == 0x90):
return response
elif (sw1 == 0x69 and sw2 == 0x82):
response, sw1, sw2 = self.__card.connection.transmit([0x00, 0x20, 0x00, 0x81, len(self.__pin)] + list(self.__pin.encode()) + [0x0])
if (sw1 == 0x90):
response, sw1, sw2 = self.__card.connection.transmit(apdu)
if (sw1 == 0x90):
return response
raise APDUResponse(sw1, sw2)
return response
def get_login_retries(self):
self.select_applet()
try:
self.send(command=0x20, p2=0x81)
except APDUResponse as e:
if (e.sw1 == 0x63 and e.sw2 & 0xF0 == 0xC0):
return e.sw2 & 0x0F
raise e
def initialize(self, pin='648219', sopin='57621880', options=None, retries=3, dkek_shares=None, puk_auts=None, puk_min_auts=None, key_domains=None):
if (retries is not None and not 0 < retries <= 10):
raise ValueError('Retries must be in the range (0,10]')
if (dkek_shares is not None and not 0 <= dkek_shares <= 10):
raise ValueError('DKEK shares must be in the range [0,10]')
if ((puk_auts is not None and puk_min_auts is None) or (puk_auts is None and puk_min_auts is not None)):
raise ValueError('PUK Auts and PUK Min Auts must be specified both')
if (puk_auts is not None and not 0 < puk_auts <= 8):
raise ValueError('PUK Auts must be in the range (0,8]')
if (puk_min_auts is not None and not 0 < puk_min_auts <= 8):
raise ValueError('PUK Min Auts must be in the range (0,8]')
if (puk_auts is not None and puk_min_auts is not None and puk_min_auts > puk_auts):
raise ValueError('PUK Min Auts must be less or equal to PUK Auts')
if (key_domains is not None and not 0 < key_domains <= 8):
raise ValueError('Key Domains must be in the range (0,8]')
a = ASN1()
if (pin is not None):
a = a.add_tag(0x81, pin.encode())
if (sopin is not None):
a = a.add_tag(0x82, sopin.encode())
if (retries is not None):
a = a.add_tag(0x91, bytes([retries]))
if (dkek_shares is not None):
a = a.add_tag(0x92, bytes([dkek_shares]))
if (puk_auts is not None and puk_min_auts is not None):
a = a.add_tag(0x93, bytes([puk_auts, puk_min_auts]))
if (key_domains is not None):
a = a.add_tag(0x97, bytes([key_domains]))
data = a.encode()
self.send(cla=0x80, command=0x50, data=data)
def login(self, pin=None):
if (pin is None):
pin = self.__pin
self.send(command=0x20, p2=0x81, data=pin.encode())
def get_first_free_id(self):
kids = self.list_keys(prefix=DOPrefixes.KEY_PREFIX)
mset = set(range(max(kids)))-set(kids)
if (len(mset) > 0):
return min(mset)
if (max(kids) == 255):
raise ValueError('Max number of key id reached')
return max(kids)+1
def list_keys(self, prefix=None):
resp = self.send(command=0x58)
if (prefix is not None):
grouped = [(resp[i],resp[i+1]) for i in range(0, len(resp), 2) if resp[i] == prefix.value]
_, kids = zip(*grouped)
return kids
return [(resp[i],resp[i+1]) for i in range(0, len(resp), 2)]
def keypair_generation(self, type, param):
a = ASN1().add_tag(0x5f29, bytes([0])).add_tag(0x42, 'UTCA00001'.encode())
if (type == KeyType.RSA):
if (not 1024 <= param <= 4096):
raise ValueError('RSA bits must be in the range [1024,4096]')
a.add_tag(0x7f49, ASN1().add_oid(oid.ID_TA_RSA_V1_5_SHA_256).add_tag(0x2, param.to_bytes(2, 'big')).encode())
elif (type == KeyType.ECC):
if (param not in ('secp192r1', 'secp256r1', 'secp384r1', 'secp521r1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp192k1', 'secp256k1')):
raise ValueError('Wrong elliptic curve name')
dom = ec_domain(Device.EcDummy(param))
pubctx = [dom.P, dom.A, dom.B, dom.G, dom.O, None, dom.F]
a.add_object(0x7f49, oid.ID_TA_ECDSA_SHA_256, pubctx)
a.add_tag(0x5f20, 'UTCDUMMY00001'.encode())
data = a.encode()
keyid = self.get_first_free_id()
self.send(command=0x46, p1=keyid, data=list(data))
return keyid
def delete_file(self, fid):
self.send(command=0xE4, data=[fid >> 8, fid & 0xff])
def public_key(self, type, keyid, param=None):
response = self.send(command=0xB1, p1=0xCE, p2=keyid, data=[0x54, 0x02, 0x00, 0x00])
cert = bytearray(response)
roid = CVC().decode(cert).pubkey().oid()
if (roid == oid.ID_TA_ECDSA_SHA_256):
curve = find_curve(ec_domain(Device.EcDummy(param)).P)
Y = bytes(CVC().decode(cert).pubkey().find(0x86).data())
return ec.EllipticCurvePublicKey.from_encoded_point(
curve,
Y,
)
elif (roid == oid.ID_TA_RSA_V1_5_SHA_256):
n = int.from_bytes(bytes(CVC().decode(cert).pubkey().find(0x81).data()), 'big')
e = int.from_bytes(bytes(CVC().decode(cert).pubkey().find(0x82).data()), 'big')
return rsa.RSAPublicNumbers(e, n).public_key()
return None
def sign(self, keyid, scheme, data):
resp = self.send(cla=0x80, command=0x68, p1=keyid, p2=scheme.value, data=data)
return resp
def verify(self, pubkey, data, signature, scheme):
if (Algorithm.ALGO_EC_RAW.value <= scheme.value <= Algorithm.ALGO_EC_SHA512.value):
if (scheme == Algorithm.ALGO_EC_SHA1):
hsh = hashes.SHA1()
elif (scheme == Algorithm.ALGO_EC_SHA224):
hsh = hashes.SHA224()
elif (scheme == Algorithm.ALGO_EC_SHA256):
hsh = hashes.SHA256()
elif (scheme == Algorithm.ALGO_EC_RAW):
hsh = utils.Prehashed(hashes.SHA512())
elif (scheme == Algorithm.ALGO_EC_SHA384):
hsh = hashes.SHA384()
elif (scheme == Algorithm.ALGO_EC_SHA512):
hsh = hashes.SHA512()
return pubkey.verify(signature, data, ec.ECDSA(hsh))
elif (Algorithm.ALGO_RSA_PKCS1_SHA1.value <= scheme.value <= Algorithm.ALGO_RSA_PSS_SHA512.value):
if (scheme == Algorithm.ALGO_RSA_PKCS1_SHA1 or scheme == Algorithm.ALGO_RSA_PSS_SHA1):
hsh = hashes.SHA1()
elif (scheme == Algorithm.ALGO_RSA_PKCS1_SHA224 or scheme == Algorithm.ALGO_RSA_PSS_SHA224):
hsh = hashes.SHA224()
elif (scheme == Algorithm.ALGO_RSA_PKCS1_SHA256 or scheme == Algorithm.ALGO_RSA_PSS_SHA256):
hsh = hashes.SHA256()
elif (scheme == Algorithm.ALGO_RSA_PKCS1_SHA384 or scheme == Algorithm.ALGO_RSA_PSS_SHA384):
hsh = hashes.SHA384()
elif (scheme == Algorithm.ALGO_RSA_PKCS1_SHA512 or scheme == Algorithm.ALGO_RSA_PSS_SHA512):
hsh = hashes.SHA512()
if (Algorithm.ALGO_RSA_PKCS1_SHA1.value <= scheme.value <= Algorithm.ALGO_RSA_PKCS1_SHA512.value):
padd = padding.PKCS1v15()
elif (Algorithm.ALGO_RSA_PSS_SHA1.value <= scheme.value <= Algorithm.ALGO_RSA_PSS_SHA512.value):
padd = padding.PSS(
mgf=padding.MGF1(hsh),
salt_length=padding.PSS.AUTO
)
return pubkey.verify(signature, data, padd, hsh)
@pytest.fixture(scope="session")
def device():
dev = Device()
return dev

View file

@ -0,0 +1,40 @@
"""
/*
* This file is part of the Pico HSM distribution (https://github.com/polhenarejos/pico-hsm).
* Copyright (c) 2022 Pol Henarejos.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"""
import pytest
from utils import KeyType, DOPrefixes
@pytest.mark.parametrize(
"curve", ['secp192r1', 'secp256r1', 'secp384r1', 'secp521r1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', 'secp192k1', 'secp256k1']
)
def test_gen_ecc(device, curve):
keyid = device.keypair_generation(KeyType.ECC, curve)
resp = device.list_keys()
assert((DOPrefixes.KEY_PREFIX.value, keyid) in resp)
device.delete_file(DOPrefixes.KEY_PREFIX.value << 8 | keyid)
@pytest.mark.parametrize(
"modulus", [1024, 2048, 4096]
)
def test_gen_rsa(device, modulus):
keyid = device.keypair_generation(KeyType.RSA, modulus)
resp = device.list_keys()
assert((DOPrefixes.KEY_PREFIX.value, keyid) in resp)
device.delete_file(DOPrefixes.KEY_PREFIX.value << 8 | keyid)

View file

@ -0,0 +1,48 @@
"""
/*
* This file is part of the Pico HSM distribution (https://github.com/polhenarejos/pico-hsm).
* Copyright (c) 2022 Pol Henarejos.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"""
import pytest
from utils import APDUResponse, SWCodes
PIN = '648219'
def test_pin(device):
device.initialize(retries=3)
retries = device.get_login_retries()
assert(retries == 3)
device.login(PIN)
with pytest.raises(APDUResponse) as e:
device.login('112233')
assert(e.value.sw1 == 0x63 and e.value.sw2 == 0xC2)
with pytest.raises(APDUResponse) as e:
device.login('112233')
assert(e.value.sw1 == 0x63 and e.value.sw2 == 0xC1)
with pytest.raises(APDUResponse) as e:
device.login('112233')
assert(e.value.sw == SWCodes.SW_PIN_BLOCKED.value)
device.initialize(retries=3)
retries = device.get_login_retries()
assert(retries == 3)

View file

@ -0,0 +1,57 @@
"""
/*
* This file is part of the Pico HSM distribution (https://github.com/polhenarejos/pico-hsm).
* Copyright (c) 2022 Pol Henarejos.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"""
import pytest
from utils import KeyType, DOPrefixes, Algorithm
from binascii import hexlify
import hashlib
data = b'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam neque urna, iaculis quis auctor scelerisque, auctor ut risus. In rhoncus, odio consequat consequat ultrices, ex libero dictum risus, accumsan interdum nisi orci ac neque. Ut vitae sem in metus hendrerit facilisis. Mauris maximus tristique mi, quis blandit lectus convallis eget.'
@pytest.mark.parametrize(
"curve", ['secp192r1', 'secp256r1', 'secp384r1', 'secp521r1',]
)
@pytest.mark.parametrize(
"scheme", [Algorithm.ALGO_EC_RAW, Algorithm.ALGO_EC_SHA1, Algorithm.ALGO_EC_SHA224, Algorithm.ALGO_EC_SHA256, Algorithm.ALGO_EC_SHA384, Algorithm.ALGO_EC_SHA512]
)
def test_signature_ecc(device, curve, scheme):
keyid = device.keypair_generation(KeyType.ECC, curve)
pubkey = device.public_key(type=KeyType.ECC, keyid=keyid, param=curve)
if (scheme == Algorithm.ALGO_EC_RAW):
datab = hashlib.sha512(data).digest()
else:
datab = data
signature = device.sign(keyid=keyid, scheme=scheme, data=datab)
device.delete_file(DOPrefixes.KEY_PREFIX.value << 8 | keyid)
device.verify(pubkey, datab, signature, scheme)
@pytest.mark.parametrize(
"modulus", [1024,2048]
)
@pytest.mark.parametrize(
"scheme", [Algorithm.ALGO_RSA_PKCS1_SHA1, Algorithm.ALGO_RSA_PKCS1_SHA224, Algorithm.ALGO_RSA_PKCS1_SHA256, Algorithm.ALGO_RSA_PKCS1_SHA384, Algorithm.ALGO_RSA_PKCS1_SHA512, Algorithm.ALGO_RSA_PSS_SHA1, Algorithm.ALGO_RSA_PSS_SHA224, Algorithm.ALGO_RSA_PSS_SHA256, Algorithm.ALGO_RSA_PSS_SHA384, Algorithm.ALGO_RSA_PSS_SHA512]
)
def test_signature_rsa(device, modulus, scheme):
keyid = device.keypair_generation(KeyType.RSA, modulus)
pubkey = device.public_key(type=KeyType.RSA, keyid=keyid)
signature = device.sign(keyid=keyid, scheme=scheme, data=data)
device.delete_file(DOPrefixes.KEY_PREFIX.value << 8 | keyid)
device.verify(pubkey, data, signature, scheme)

120
tests/utils.py Normal file
View file

@ -0,0 +1,120 @@
"""
/*
* This file is part of the Pico HSM distribution (https://github.com/polhenarejos/pico-hsm).
* Copyright (c) 2022 Pol Henarejos.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"""
from enum import Enum
class SWCodes(Enum):
SW_BYTES_REMAINING_00 = 0x6100
SW_WARNING_STATE_UNCHANGED = 0x6200
SW_WARNING_CORRUPTED = 0x6281
SW_WARNING_EOF = 0x6282
SW_WARNING_EF_DEACTIVATED = 0x6283
SW_WARNING_WRONG_FCI = 0x6284
SW_WARNING_EF_TERMINATED = 0x6285
SW_WARNING_NOINFO = 0x6300
SW_WARNING_FILLUP = 0x6381
SW_EXEC_ERROR = 0x6400
SW_SECURE_MESSAGE_EXEC_ERROR = 0x6600
SW_WRONG_LENGTH = 0x6700
SW_LOGICAL_CHANNEL_NOT_SUPPORTED = 0x6881
SW_SECURE_MESSAGING_NOT_SUPPORTED = 0x6882
SW_COMMAND_INCOMPATIBLE = 0x6981
SW_SECURITY_STATUS_NOT_SATISFIED = 0x6982
SW_PIN_BLOCKED = 0x6983
SW_DATA_INVALID = 0x6984
SW_CONDITIONS_NOT_SATISFIED = 0x6985
SW_COMMAND_NOT_ALLOWED = 0x6986
SW_SECURE_MESSAGING_MISSING_DO = 0x6987
SW_SECURE_MESSAGING_INCORRECT_DO = 0x6988
SW_APPLET_SELECT_FAILED = 0x6999
SW_INCORRECT_PARAMS = 0x6A80
SW_FUNC_NOT_SUPPORTED = 0x6A81
SW_FILE_NOT_FOUND = 0x6A82
SW_RECORD_NOT_FOUND = 0x6A83
SW_FILE_FULL = 0x6A84
SW_WRONG_NE = 0x6A85
SW_INCORRECT_P1P2 = 0x6A86
SW_WRONG_NC = 0x6A87
SW_REFERENCE_NOT_FOUND = 0x6A88
SW_FILE_EXISTS = 0x6A89
SW_WRONG_P1P2 = 0x6B00
SW_CORRECT_LENGTH_00 = 0x6C00
SW_INS_NOT_SUPPORTED = 0x6D00
SW_CLA_NOT_SUPPORTED = 0x6E00
SW_UNKNOWN = 0x6F00
SW_OK = 0x900
class APDUResponse(Exception):
def __init__(self, sw1, sw2):
self.sw1 = sw1
self.sw2 = sw2
self.sw = sw1 << 8 | sw2
super().__init__(f'SW:{sw1:02X}{sw2:02X}')
class DOPrefixes(Enum):
PRKD_PREFIX = 0xC4
CD_PREFIX = 0xC8
DCOD_PREFIX = 0xC9
CA_CERTIFICATE_PREFIX = 0xCA
KEY_PREFIX = 0xCC
PROT_DATA_PREFIX = 0xCD
EE_CERTIFICATE_PREFIX = 0xCE
DATA_PREFIX = 0xCF
class KeyType(Enum):
RSA = 1
ECC = 2
class Algorithm(Enum):
ALGO_EC_RAW = 0x70
ALGO_EC_SHA1 = 0x71
ALGO_EC_SHA224 = 0x72
ALGO_EC_SHA256 = 0x73
ALGO_EC_SHA384 = 0x74
ALGO_EC_SHA512 = 0x75
ALGO_RSA_RAW = 0x20
ALGO_RSA_DECRYPT = 0x21
ALGO_RSA_DECRYPT_PKCS1 = 0x22
ALGO_RSA_DECRYPT_OEP = 0x23
ALGO_RSA_PKCS1 = 0x30
ALGO_RSA_PKCS1_SHA1 = 0x31
ALGO_RSA_PKCS1_SHA224 = 0x32
ALGO_RSA_PKCS1_SHA256 = 0x33
ALGO_RSA_PKCS1_SHA384 = 0x34
ALGO_RSA_PKCS1_SHA512 = 0x35
ALGO_RSA_PSS = 0x40
ALGO_RSA_PSS_SHA1 = 0x41
ALGO_RSA_PSS_SHA224 = 0x42
ALGO_RSA_PSS_SHA256 = 0x43
ALGO_RSA_PSS_SHA384 = 0x44
ALGO_RSA_PSS_SHA512 = 0x45