diff --git a/tests/pico-hsm/test_011_security_regressions.py b/tests/pico-hsm/test_011_security_regressions.py new file mode 100644 index 0000000..48bee82 --- /dev/null +++ b/tests/pico-hsm/test_011_security_regressions.py @@ -0,0 +1,131 @@ +""" +/* + * 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 . + */ +""" + +import pytest + +from picokey import APDUResponse, SWCodes +from picohsm.DO import DOPrefixes +from picohsm.const import DEFAULT_PIN + + +def raw_send(device, command, cla=0x00, p1=0x00, p2=0x00, data=None, ne=None): + # Use low-level transport to avoid automatic PIN retry/login behavior. + return device._PicoHSM__card.send(command=command, cla=cla, p1=p1, p2=p2, data=data, ne=ne, codes=[]) + + +def read_binary_raw(device, fid): + return raw_send( + device, + command=0xB1, + p1=(fid >> 8) & 0xFF, + p2=fid & 0xFF, + data=[0x54, 0x02, 0x00, 0x00], + ne=0, + ) + + +def test_01_protected_data_requires_pin_for_read(device): + fid = (DOPrefixes.PROT_DATA_PREFIX << 8) | 0x01 + payload = b"protected-regression" + + device.initialize() + device.login(DEFAULT_PIN) + device.put_contents(p1=fid, data=payload) + device.logout() + + with pytest.raises(APDUResponse) as e: + read_binary_raw(device, fid) + assert e.value.sw == SWCodes.SW_SECURITY_STATUS_NOT_SATISFIED + + device.login(DEFAULT_PIN) + data, sw = read_binary_raw(device, fid) + assert sw == 0x9000 + assert bytes(data) == payload + + +def test_02_static_sensitive_files_are_not_readable(device): + device.initialize() + device.logout() + + for fid in (0x1081, 0x100E, 0x100A, 0x100B): + with pytest.raises(APDUResponse) as e: + read_binary_raw(device, fid) + assert e.value.sw == SWCodes.SW_SECURITY_STATUS_NOT_SATISFIED + + +def test_03_key_object_readout_is_blocked_even_when_authenticated(device): + # #3 depends on #2 class of bug: private key material must not be readable. + # KEY_PREFIX objects are blocked by policy for READ BINARY. + device.initialize() + device.logout() + + with pytest.raises(APDUResponse) as e: + read_binary_raw(device, 0xCC00) # EF_KEY_DEV + assert e.value.sw in (SWCodes.SW_SECURITY_STATUS_NOT_SATISFIED, SWCodes.SW_FILE_NOT_FOUND) + + device.login(DEFAULT_PIN) + with pytest.raises(APDUResponse) as e: + read_binary_raw(device, 0xCC00) # EF_KEY_DEV + assert e.value.sw in (SWCodes.SW_SECURITY_STATUS_NOT_SATISFIED, SWCodes.SW_FILE_NOT_FOUND) + + +def test_04_otp_extra_command_is_not_available(device): + # #4: OTP command path was removed. + device.initialize() + device.login(DEFAULT_PIN) + with pytest.raises(APDUResponse) as e: + raw_send(device, cla=0x80, command=0x64, p1=0x4C, p2=0x00, data=[0x00, 0x00]) + assert e.value.sw == SWCodes.SW_INCORRECT_P1P2 + + +def test_04_session_pin_instruction_removed(device): + with pytest.raises(APDUResponse) as e: + raw_send(device, command=0x5A, p1=0x01, p2=0x81) + assert e.value.sw1 == 0x6D and e.value.sw2 == 0x00 + + +def test_06_update_ef_rejects_out_of_bounds_offset(device): + fid = (DOPrefixes.DATA_PREFIX << 8) | 0x10 + device.initialize() + device.login(DEFAULT_PIN) + device.put_contents(p1=fid, data=b"0123456789abcdef") + + # offset=4030, len=8 => 4038 (>4032) must be rejected. + data = [0x54, 0x02, 0x0F, 0xBE, 0x53, 0x08] + [0xAA] * 8 + with pytest.raises(APDUResponse) as e: + raw_send(device, command=0xD7, p1=(fid >> 8) & 0xFF, p2=fid & 0xFF, data=data) + assert e.value.sw1 == 0x67 and e.value.sw2 == 0x00 + + +def test_07_secure_messaging_requires_valid_mac(device): + device.initialize() + device.logout() + + # GA must fail without an authenticated session. + with pytest.raises(APDUResponse) as e: + device.general_authentication() + assert e.value.sw1 == 0x64 and e.value.sw2 == 0x00 + + # After PIN verification, GA should be available and SM can be established. + device.login(DEFAULT_PIN) + device.general_authentication() + + with pytest.raises(APDUResponse) as e: + raw_send(device, command=0x84, cla=0x0C, data=[0x97, 0x01, 0x10], ne=0) + assert e.value.sw1 == 0x69 and e.value.sw2 in (0x84, 0x87, 0x88) diff --git a/tests/scripts/pkcs11.sh b/tests/scripts/pkcs11.sh index c688fdc..690b3f0 100755 --- a/tests/scripts/pkcs11.sh +++ b/tests/scripts/pkcs11.sh @@ -50,6 +50,13 @@ test $? -eq 0 || { exit 1 } +echo "==== Test PKCS11 security regressions ====" +./tests/scripts/pkcs11_security_regressions.sh +test $? -eq 0 || { + echo -e "\t${FAIL}" + exit 1 +} + echo "==== Test backup and restore ====" ./tests/scripts/backup.sh test $? -eq 0 || { diff --git a/tests/scripts/pkcs11_security_regressions.sh b/tests/scripts/pkcs11_security_regressions.sh new file mode 100755 index 0000000..5fed5c7 --- /dev/null +++ b/tests/scripts/pkcs11_security_regressions.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +source ./tests/scripts/func.sh + +TMP_SIGN_DATA=".pkcs11_sec_reg_data" +TMP_PRIV_DATA=".pkcs11_sec_reg_priv_data" +TMP_SIG_OUT=".pkcs11_sec_reg.sig" + +cleanup() { + rm -f "$TMP_SIGN_DATA" "$TMP_PRIV_DATA" "$TMP_SIG_OUT" + pkcs11-tool -l --pin 648219 --delete-object --type privkey --id 1 > /dev/null 2>&1 || true + pkcs11-tool -l --pin 648219 --delete-object --type data --label 'sec_priv_data' > /dev/null 2>&1 || true +} + +trap cleanup EXIT + +reset +test $? -eq 0 || exit $? + +echo "security regression data" > "$TMP_SIGN_DATA" + +echo -n " Security regression: private key operation requires login..." +pkcs11-tool -l --pin 648219 --keypairgen --key-type rsa:2048 --id 1 --label "SecRegression" > /dev/null 2>&1 +test $? -eq 0 && echo -n "." || exit $? +e=$(pkcs11-tool --id 1 --sign --mechanism RSA-PKCS -i "$TMP_SIGN_DATA" -o "$TMP_SIG_OUT" 2>&1) +test $? -ne 0 && echo -n "." || exit $? +( + grep -q "CKR_USER_NOT_LOGGED_IN" <<< "$e" || + grep -q "CKR_PIN_REQUIRED" <<< "$e" || + grep -q "util_getpass error" <<< "$e" +) && echo -e ".\t${OK}" || exit $? + +echo -n " Security regression: private key material is not exportable..." +e=$(pkcs11-tool --read-object --type privkey --id 1 --pin 648219 2>&1) +test $? -eq 0 && echo -n "." || exit $? +( + grep -q "CKR_ATTRIBUTE_SENSITIVE" <<< "$e" || + grep -q "CKR_ACTION_PROHIBITED" <<< "$e" || + grep -q "reading private keys not (yet) supported" <<< "$e" || + grep -q "error: object not found" <<< "$e" +) && echo -e ".\t${OK}" || exit $? + +echo -n " Security regression: private data object cannot be read without login..." +echo "private data regression" > "$TMP_PRIV_DATA" +pkcs11-tool --pin 648219 --write-object "$TMP_PRIV_DATA" --type data --id 2 --label 'sec_priv_data' --private > /dev/null 2>&1 +test $? -eq 0 && echo -n "." || exit $? +e=$(pkcs11-tool --read-object --type data --label 'sec_priv_data' 2>&1) +test $? -eq 1 && echo -n "." || exit $? +( + grep -q "error: object not found" <<< "$e" || + grep -q "CKR_USER_NOT_LOGGED_IN" <<< "$e" || + grep -q "CKR_PIN_REQUIRED" <<< "$e" +) && echo -n "." || exit $? +e=$(pkcs11-tool --read-object --type data --label 'sec_priv_data' --pin 648219 2>&1) +test $? -eq 0 && echo -n "." || exit $? +grep -q "private data regression" <<< "$e" && echo -e ".\t${OK}" || exit $?