mirror of
https://github.com/polhenarejos/pico-hsm.git
synced 2026-04-17 13:48:27 +00:00
Add regression tests.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
parent
1b322755a1
commit
f1d927d4ef
3 changed files with 194 additions and 0 deletions
131
tests/pico-hsm/test_011_security_regressions.py
Normal file
131
tests/pico-hsm/test_011_security_regressions.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
@ -50,6 +50,13 @@ test $? -eq 0 || {
|
||||||
exit 1
|
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 ===="
|
echo "==== Test backup and restore ===="
|
||||||
./tests/scripts/backup.sh
|
./tests/scripts/backup.sh
|
||||||
test $? -eq 0 || {
|
test $? -eq 0 || {
|
||||||
|
|
|
||||||
56
tests/scripts/pkcs11_security_regressions.sh
Executable file
56
tests/scripts/pkcs11_security_regressions.sh
Executable file
|
|
@ -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 $?
|
||||||
Loading…
Add table
Reference in a new issue