diff --git a/CMakeLists.txt b/CMakeLists.txt index 136ac3d..292c0cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ cmake_minimum_required(VERSION 3.13) option(OPENPGP_TEST_INIT_LEGACY_PIN "Bootstrap legacy PIN/DEK format for migration tests" OFF) +option(ENABLE_ADMINLESS_MODE "Enable the non-standard Gnuk-compatible admin-less PIN mode" ON) set(USB_VID 0x2E8A) set(USB_PID 0x10FF) @@ -93,6 +94,9 @@ if(NOT ESP_PLATFORM) if(OPENPGP_TEST_INIT_LEGACY_PIN) target_compile_definitions(pico_openpgp PRIVATE OPENPGP_TEST_INIT_LEGACY_PIN=1) endif() + if(ENABLE_ADMINLESS_MODE) + target_compile_definitions(pico_openpgp PRIVATE ENABLE_ADMINLESS_MODE=1) + endif() set(COMMON_COMPILE_OPTIONS -Wall diff --git a/src/openpgp/CMakeLists.txt b/src/openpgp/CMakeLists.txt index 81bd76c..6caac6f 100644 --- a/src/openpgp/CMakeLists.txt +++ b/src/openpgp/CMakeLists.txt @@ -6,4 +6,7 @@ idf_component_register( if(OPENPGP_TEST_INIT_LEGACY_PIN) target_compile_definitions(${COMPONENT_LIB} PRIVATE OPENPGP_TEST_INIT_LEGACY_PIN=1) endif() +if(ENABLE_ADMINLESS_MODE) + target_compile_definitions(${COMPONENT_LIB} PRIVATE ENABLE_ADMINLESS_MODE=1) +endif() idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE ON) diff --git a/src/openpgp/cmd_change_pin.c b/src/openpgp/cmd_change_pin.c index cb196f1..6ce24e6 100644 --- a/src/openpgp/cmd_change_pin.c +++ b/src/openpgp/cmd_change_pin.c @@ -18,10 +18,38 @@ #include "openpgp.h" #include "otp.h" +#ifdef ENABLE_ADMINLESS_MODE +#include "mbedtls/constant_time.h" + +static bool pw3_matches_nonfactory_pw1(const uint8_t *pin, size_t pin_len) { + file_t *pw1 = file_search_by_fid(EF_PW1, NULL, SPECIFY_EF); + uint8_t verifier[34]; + + if (!pw1 || !file_has_data(pw1) || file_get_data(pw1)[0] < 8) { + return false; + } + if (file_get_size(pw1) == 33) { + verifier[0] = pin_len; + double_hash_pin(pin, pin_len, verifier + 1); + return mbedtls_ct_memcmp(file_get_data(pw1), verifier, 33) == 0; + } + if (file_get_size(pw1) == 34) { + verifier[0] = pin_len; + verifier[1] = 0x1; + pin_derive_verifier(pin, pin_len, verifier + 2); + return mbedtls_ct_memcmp(file_get_data(pw1), verifier, sizeof(verifier)) == 0; + } + return false; +} +#endif + int cmd_change_pin(void) { if (P1(apdu) != 0x0) { return SW_WRONG_P1P2(); } + if (P2(apdu) != 0x81 && P2(apdu) != 0x83) { + return SW_REFERENCE_NOT_FOUND(); + } uint16_t fid = 0x1000 | P2(apdu); file_t *pw; if (!(pw = file_search_by_fid(fid, NULL, SPECIFY_EF))) { @@ -37,11 +65,28 @@ int cmd_change_pin(void) { return SW_EXEC_ERROR(); } + const uint8_t *new_pin = apdu.data + pin_len; + size_t new_pin_len = apdu.nc - pin_len; +#ifdef ENABLE_ADMINLESS_MODE + /* Empty PW3 is Gnuk's "PW3 not configured" transition. Keep its + * current verifier so KDF-backed host flows can still verify it. */ + bool clear_pw3 = P2(apdu) == 0x83 && new_pin_len == 0; + if (clear_pw3) { + new_pin = apdu.data; + new_pin_len = pin_len; + } + bool sync_adminless_pw3 = P2(apdu) == 0x81 && (openpgp_adminless_is_active() || (openpgp_adminless_is_pending() && new_pin_len >= 8)); + bool disable_pending_adminless = P2(apdu) == 0x81 && openpgp_adminless_is_pending() && new_pin_len < 8; + bool enable_adminless = P2(apdu) == 0x83 && !clear_pw3 && pw3_matches_nonfactory_pw1(new_pin, new_pin_len); +#endif + uint8_t dhash[34]; - dhash[0] = apdu.nc - pin_len; + dhash[0] = new_pin_len; dhash[1] = 0x1; // Format - pin_derive_verifier(apdu.data + pin_len, apdu.nc - pin_len, dhash + 2); - file_put_data(pw, dhash, sizeof(dhash)); + pin_derive_verifier(new_pin, new_pin_len, dhash + 2); + if ((r = file_put_data(pw, dhash, sizeof(dhash))) != PICOKEYS_OK) { + return SW_MEMORY_FAILURE(); + } if (P2(apdu) == 0x81) { file_t *tf = file_search_by_fid(EF_DEK_PW1, NULL, SPECIFY_EF); @@ -50,9 +95,22 @@ int cmd_change_pin(void) { } uint8_t def[DEK_FILE_SIZE]; def[0] = 0x3; - pin_derive_session(apdu.data + pin_len, apdu.nc - pin_len, session_pw1); - encrypt_with_aad(session_pw1, dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, def + 1); + pin_derive_session(new_pin, new_pin_len, session_pw1); + if ((r = encrypt_with_aad(session_pw1, dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, def + 1)) != PICOKEYS_OK) { + return SW_EXEC_ERROR(); + } r = file_put_data(tf, def, sizeof(def)); +#ifdef ENABLE_ADMINLESS_MODE + if (r == PICOKEYS_OK && sync_adminless_pw3) { + r = openpgp_adminless_sync_pw3(new_pin, new_pin_len, dhash); + if (r == PICOKEYS_OK && openpgp_adminless_is_pending()) { + r = openpgp_adminless_enable(); + } + } + else if (r == PICOKEYS_OK && disable_pending_adminless) { + r = openpgp_adminless_disable(); + } +#endif } else if (P2(apdu) == 0x83) { file_t *tf = file_search_by_fid(EF_DEK_PW3, NULL, SPECIFY_EF); @@ -61,9 +119,19 @@ int cmd_change_pin(void) { } uint8_t def[DEK_FILE_SIZE]; def[0] = 0x3; - pin_derive_session(apdu.data + pin_len, apdu.nc - pin_len, session_pw3); - encrypt_with_aad(session_pw3, dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, def + 1); + pin_derive_session(new_pin, new_pin_len, session_pw3); + if ((r = encrypt_with_aad(session_pw3, dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, def + 1)) != PICOKEYS_OK) { + return SW_EXEC_ERROR(); + } r = file_put_data(tf, def, sizeof(def)); +#ifdef ENABLE_ADMINLESS_MODE + if (r == PICOKEYS_OK) { + r = enable_adminless ? openpgp_adminless_enable() : clear_pw3 ? openpgp_adminless_reset() : openpgp_adminless_disable(); + } +#endif + } + if (r != PICOKEYS_OK) { + return SW_MEMORY_FAILURE(); } flash_commit(); return SW_OK(); diff --git a/src/openpgp/cmd_put_data.c b/src/openpgp/cmd_put_data.c index d26f8fc..9929723 100644 --- a/src/openpgp/cmd_put_data.c +++ b/src/openpgp/cmd_put_data.c @@ -88,7 +88,9 @@ int cmd_put_data(void) { dhash[0] = apdu.nc; dhash[1] = 0x1; // Format pin_derive_verifier(apdu.data, apdu.nc, dhash + 2); - file_put_data(ef, dhash, sizeof(dhash)); + if ((r = file_put_data(ef, dhash, sizeof(dhash))) != PICOKEYS_OK) { + return SW_MEMORY_FAILURE(); + } file_t *tf = file_search_by_fid(EF_DEK_RC, NULL, SPECIFY_EF); if (!tf) { @@ -100,6 +102,9 @@ int cmd_put_data(void) { pin_derive_session(apdu.data, apdu.nc, session_rc); encrypt_with_aad(session_rc, dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, def + 1); r = file_put_data(tf, def, sizeof(def)); + if (r == PICOKEYS_OK) { + r = pin_reset_retries(ef, true); + } } else { r = file_put_data(ef, apdu.data, apdu.nc); @@ -107,6 +112,13 @@ int cmd_put_data(void) { if (r != PICOKEYS_OK) { return SW_MEMORY_FAILURE(); } +#ifdef ENABLE_ADMINLESS_MODE + if (requested_fid == EF_KDF && apdu.nc > 0 && !(apdu.nc == 3 && memcmp(apdu.data, "\x81\x01\x00", 3) == 0)) { + if ((r = openpgp_adminless_begin_kdf_migration()) != PICOKEYS_OK) { + return SW_MEMORY_FAILURE(); + } + } +#endif flash_commit(); } else { diff --git a/src/openpgp/cmd_reset_retry.c b/src/openpgp/cmd_reset_retry.c index e30d497..59764fc 100644 --- a/src/openpgp/cmd_reset_retry.c +++ b/src/openpgp/cmd_reset_retry.c @@ -25,6 +25,9 @@ int cmd_reset_retry(void) { if (P1(apdu) == 0x0 || P1(apdu) == 0x2) { int newpin_len = 0; file_t *pw = NULL; +#ifdef ENABLE_ADMINLESS_MODE + bool sync_adminless_pw3 = openpgp_adminless_is_active(); +#endif has_pw1 = false; if (!(pw = file_search_by_fid(EF_PW1, NULL, SPECIFY_EF))) { return SW_REFERENCE_NOT_FOUND(); @@ -72,7 +75,14 @@ int cmd_reset_retry(void) { dhash[0] = newpin_len; dhash[1] = 0x1; // Format pin_derive_verifier(apdu.data + (apdu.nc - newpin_len), newpin_len, dhash + 2); - file_put_data(pw, dhash, sizeof(dhash)); + if ((r = file_put_data(pw, dhash, sizeof(dhash))) != PICOKEYS_OK) { + return SW_MEMORY_FAILURE(); + } +#ifdef ENABLE_ADMINLESS_MODE + if (sync_adminless_pw3 && (r = openpgp_adminless_sync_pw3(apdu.data + (apdu.nc - newpin_len), newpin_len, dhash)) != PICOKEYS_OK) { + return SW_MEMORY_FAILURE(); + } +#endif if (pin_reset_retries(pw, true) != PICOKEYS_OK) { return SW_MEMORY_FAILURE(); } diff --git a/src/openpgp/cmd_verify.c b/src/openpgp/cmd_verify.c index fa0404d..cae7d4f 100644 --- a/src/openpgp/cmd_verify.c +++ b/src/openpgp/cmd_verify.c @@ -27,6 +27,11 @@ int cmd_verify(void) { } if (p2 == 0x81) { has_pw1 = false; +#ifdef ENABLE_ADMINLESS_MODE + if (openpgp_adminless_is_active()) { + has_pw3 = false; + } +#endif } else if (p2 == 0x82) { has_pw2 = false; @@ -54,7 +59,23 @@ int cmd_verify(void) { return SW_REFERENCE_NOT_FOUND(); } if (apdu.nc > 0) { - return check_pin(pw, apdu.data, apdu.nc); +#ifdef ENABLE_ADMINLESS_MODE + if (p2 == 0x81 && openpgp_adminless_is_active()) { + has_pw1 = false; + has_pw3 = false; + } + else if (p2 == 0x83 && openpgp_adminless_is_active() && !has_pw1) { + has_pw3 = false; + } +#endif + uint16_t r = check_pin(pw, apdu.data, apdu.nc); +#ifdef ENABLE_ADMINLESS_MODE + /* Gnuk reports an unavailable PW3 as a security-status failure. */ + if (p2 == 0x83 && openpgp_adminless_is_active() && r != SW_OK()) { + return SW_SECURITY_STATUS_NOT_SATISFIED(); + } +#endif + return r; } uint8_t retries = *(file_get_data(pw_status) + 3 + (fid & 0xf)); if (retries == 0) { diff --git a/src/openpgp/openpgp.c b/src/openpgp/openpgp.c index dd4aabf..ebc3c79 100644 --- a/src/openpgp/openpgp.c +++ b/src/openpgp/openpgp.c @@ -48,6 +48,132 @@ uint8_t dek[DEK_SIZE]; uint16_t algo_dec = EF_ALGO_PRIV2, algo_aut = EF_ALGO_PRIV3, pk_dec = EF_PK_DEC, pk_aut = EF_PK_AUT; extern bool is_gpg; +#ifdef ENABLE_ADMINLESS_MODE +enum { + ADMINLESS_MODE_PENDING = 0, + ADMINLESS_MODE_ENABLED = 1, + ADMINLESS_MODE_DISABLED = 2, + ADMINLESS_MODE_KDF_MIGRATION = 3, +}; + +#define ADMINLESS_MODE_OFFSET 6 +#define ADMINLESS_RETRIES_SIZE (ADMINLESS_MODE_OFFSET + 1) + +static int adminless_set_mode(uint8_t mode) { + file_t *ef = file_search_by_fid(EF_PW_RETRIES, NULL, SPECIFY_EF); + if (!ef) { + return PICOKEYS_ERR_FILE_NOT_FOUND; + } + uint8_t retries[16] = { 0x1, 3, 3, 3, 3, 3, ADMINLESS_MODE_DISABLED }; + uint16_t retries_len = ADMINLESS_RETRIES_SIZE; + if (file_has_data(ef)) { + retries_len = MAX(file_get_size(ef), retries_len); + if (retries_len > sizeof(retries)) { + return PICOKEYS_ERR_NO_MEMORY; + } + memcpy(retries, file_get_data(ef), file_get_size(ef)); + } + retries[ADMINLESS_MODE_OFFSET] = mode; + return file_put_data(ef, retries, retries_len); +} + +static bool adminless_mode_is(uint8_t mode) { + file_t *ef = file_search_by_fid(EF_PW_RETRIES, NULL, SPECIFY_EF); + return ef && file_has_data(ef) && file_get_size(ef) >= ADMINLESS_RETRIES_SIZE && file_get_data(ef)[ADMINLESS_MODE_OFFSET] == mode; +} + +static bool pin_is_factory_default(const file_t *pin, const uint8_t *factory_pin, size_t factory_pin_len) { + uint8_t verifier[34]; + + if (!pin || !file_has_data(pin)) { + return false; + } + if (file_get_size(pin) == 33) { + verifier[0] = factory_pin_len; + double_hash_pin(factory_pin, factory_pin_len, verifier + 1); + return mbedtls_ct_memcmp(file_get_data(pin), verifier, 33) == 0; + } + if (file_get_size(pin) == 34) { + verifier[0] = factory_pin_len; + verifier[1] = 0x1; + pin_derive_verifier(factory_pin, factory_pin_len, verifier + 2); + return mbedtls_ct_memcmp(file_get_data(pin), verifier, sizeof(verifier)) == 0; + } + return false; +} + +static bool pw1_and_pw3_are_factory_default(void) { + static const uint8_t factory_pw1[] = "123456"; + static const uint8_t factory_pw3[] = "12345678"; + + return pin_is_factory_default(file_search_by_fid(EF_PW1, NULL, SPECIFY_EF), factory_pw1, sizeof(factory_pw1) - 1) + && pin_is_factory_default(file_search_by_fid(EF_PW3, NULL, SPECIFY_EF), factory_pw3, sizeof(factory_pw3) - 1); +} + +bool openpgp_adminless_is_pending(void) { + return adminless_mode_is(ADMINLESS_MODE_PENDING); +} + +bool openpgp_adminless_is_active(void) { + return adminless_mode_is(ADMINLESS_MODE_ENABLED); +} + +int openpgp_adminless_begin_kdf_migration(void) { + if (!openpgp_adminless_is_pending()) { + return PICOKEYS_OK; + } + return adminless_set_mode(ADMINLESS_MODE_KDF_MIGRATION); +} + +int openpgp_adminless_sync_pw3(const uint8_t *pin, size_t pin_len, const uint8_t verifier[34]) { + file_t *pw3 = file_search_by_fid(EF_PW3, NULL, SPECIFY_EF); + file_t *dek_pw3 = file_search_by_fid(EF_DEK_PW3, NULL, SPECIFY_EF); + if (!pw3 || !dek_pw3) { + return PICOKEYS_ERR_FILE_NOT_FOUND; + } + int r = file_put_data(pw3, verifier, 34); + if (r != PICOKEYS_OK) { + return r; + } + uint8_t encrypted_dek[DEK_FILE_SIZE]; + encrypted_dek[0] = 0x3; + pin_derive_session(pin, pin_len, session_pw3); + r = encrypt_with_aad(session_pw3, dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, encrypted_dek + 1); + if (r != PICOKEYS_OK) { + return r; + } + return file_put_data(dek_pw3, encrypted_dek, sizeof(encrypted_dek)); +} + +int openpgp_adminless_enable(void) { + file_t *pw_status = file_search_by_fid(EF_PW_PRIV, NULL, SPECIFY_EF); + if (!pw_status || !file_has_data(pw_status) || file_get_size(pw_status) == 0 || file_get_size(pw_status) > 16) { + return PICOKEYS_ERR_FILE_NOT_FOUND; + } + uint8_t status[16]; + uint16_t status_len = file_get_size(pw_status); + memcpy(status, file_get_data(pw_status), status_len); + status[0] = 0x0; // Require PW1 for every signature in admin-less mode. + int r = file_put_data(pw_status, status, status_len); + if (r != PICOKEYS_OK) { + return r; + } + r = adminless_set_mode(ADMINLESS_MODE_ENABLED); + if (r == PICOKEYS_OK && has_pw1) { + has_pw3 = true; + } + return r; +} + +int openpgp_adminless_disable(void) { + return adminless_set_mode(ADMINLESS_MODE_DISABLED); +} + +int openpgp_adminless_reset(void) { + return adminless_set_mode(ADMINLESS_MODE_PENDING); +} +#endif + uint8_t openpgp_aid[] = { 6, 0xD2, 0x76, 0x00, 0x01, 0x24, 0x01, @@ -147,7 +273,6 @@ void scan_files_openpgp(void) { pin_derive_session(def3, sizeof(def3), session_pw3); encrypt_with_aad(session_pw3, random_dek, DEK_SIZE, PIN_KDF_DEFAULT_VERSION, def + 1); mbedtls_platform_zeroize(session_pw3, sizeof(session_pw3)); - file_put_data(ef_dek_rc, def, sizeof(def)); file_put_data(ef_dek_pw3, def, sizeof(def)); #endif @@ -216,12 +341,24 @@ void scan_files_openpgp(void) { file_put_data(ef, def, sizeof(def)); } } +#ifdef ENABLE_ADMINLESS_MODE + uint8_t legacy_adminless_mode = ADMINLESS_MODE_DISABLED; + bool migrate_legacy_adminless_mode = false; +#endif if ((ef = file_search_by_fid(EF_PW_PRIV, NULL, SPECIFY_ANY))) { if (!ef->data) { printf("PW status is empty. Initializing to default\r\n"); const uint8_t def[] = { 0x1, 127, 127, 127, 3, 3, 3 }; file_put_data(ef, def, sizeof(def)); } +#ifdef ENABLE_ADMINLESS_MODE + else if (file_get_size(ef) == 8) { + /* Migration from the unreleased PW-status-byte implementation. */ + legacy_adminless_mode = file_get_data(ef)[7]; + migrate_legacy_adminless_mode = legacy_adminless_mode <= ADMINLESS_MODE_KDF_MIGRATION; + file_put_data(ef, file_get_data(ef), 7); + } +#endif } if ((ef = file_search_by_fid(EF_UIF_SIG, NULL, SPECIFY_ANY))) { if (!ef->data) { @@ -259,10 +396,28 @@ void scan_files_openpgp(void) { } } if ((ef = file_search_by_fid(EF_PW_RETRIES, NULL, SPECIFY_ANY))) { - if (!ef->data) { + if (!ef->data +#ifdef ENABLE_ADMINLESS_MODE + || reset_dek || file_get_size(ef) < ADMINLESS_RETRIES_SIZE +#endif + ) { printf("PW retries is empty. Initializing to default\r\n"); +#ifdef ENABLE_ADMINLESS_MODE + uint8_t def[ADMINLESS_RETRIES_SIZE] = { 0x1, 3, 3, 3, 3, 3, ADMINLESS_MODE_DISABLED }; + if (file_has_data(ef) && !reset_dek) { + memcpy(def, file_get_data(ef), MIN(file_get_size(ef), ADMINLESS_MODE_OFFSET)); + } + if (migrate_legacy_adminless_mode) { + def[ADMINLESS_MODE_OFFSET] = legacy_adminless_mode; + } + else if (pw1_and_pw3_are_factory_default()) { + def[ADMINLESS_MODE_OFFSET] = ADMINLESS_MODE_PENDING; + } + file_put_data(ef, def, sizeof(def)); +#else const uint8_t def[] = { 0x1, 3, 3, 3 }; file_put_data(ef, def, sizeof(def)); +#endif } } flash_commit(); @@ -759,6 +914,11 @@ int check_pin(const file_t *pin, const uint8_t *data, size_t len) { if (pin->fid == EF_PW1) { if (P2(apdu) == 0x81) { has_pw1 = true; +#ifdef ENABLE_ADMINLESS_MODE + if (openpgp_adminless_is_active()) { + has_pw3 = true; + } +#endif } else { has_pw2 = true; @@ -779,6 +939,11 @@ int inc_sig_count(void) { } if (file_get_data(pw_status)[0] == 0) { has_pw1 = false; +#ifdef ENABLE_ADMINLESS_MODE + if (openpgp_adminless_is_active()) { + has_pw3 = false; + } +#endif } file_t *ef = file_search_by_fid(EF_SIG_COUNT, NULL, SPECIFY_ANY); if (!ef || !ef->data) { diff --git a/src/openpgp/openpgp.h b/src/openpgp/openpgp.h index f21e474..4523852 100644 --- a/src/openpgp/openpgp.h +++ b/src/openpgp/openpgp.h @@ -61,6 +61,15 @@ extern void select_file(file_t *pe); extern int parse_do(uint16_t *fids, int mode); extern int load_dek(void); extern int check_pin(const file_t *pin, const uint8_t *data, size_t len); +#ifdef ENABLE_ADMINLESS_MODE +extern bool openpgp_adminless_is_pending(void); +extern bool openpgp_adminless_is_active(void); +extern int openpgp_adminless_begin_kdf_migration(void); +extern int openpgp_adminless_sync_pw3(const uint8_t *pin, size_t pin_len, const uint8_t verifier[34]); +extern int openpgp_adminless_enable(void); +extern int openpgp_adminless_disable(void); +extern int openpgp_adminless_reset(void); +#endif extern mbedtls_ecp_group_id get_ec_group_id_from_attr(const uint8_t *algo, size_t algo_len); extern int reset_sig_count(void); extern uint16_t algo_dec, algo_aut, pk_dec, pk_aut; diff --git a/tests/010_kdfnone/test_040_adminless_kdfnone.py b/tests/010_kdfnone/test_040_adminless_kdfnone.py index c6fb3c0..d26fa5f 100644 --- a/tests/010_kdfnone/test_040_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_040_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_personalize_admin_less_1 import * diff --git a/tests/010_kdfnone/test_041_adminless_kdfnone.py b/tests/010_kdfnone/test_041_adminless_kdfnone.py index 9e864ba..380e7a1 100644 --- a/tests/010_kdfnone/test_041_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_041_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_public_key_operations_alt import * diff --git a/tests/010_kdfnone/test_042_adminless_kdfnone.py b/tests/010_kdfnone/test_042_adminless_kdfnone.py index 5d93e0a..16582f4 100644 --- a/tests/010_kdfnone/test_042_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_042_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_ds_counter1 import * diff --git a/tests/010_kdfnone/test_043_adminless_kdfnone.py b/tests/010_kdfnone/test_043_adminless_kdfnone.py index 74d7da6..ea3dd5c 100644 --- a/tests/010_kdfnone/test_043_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_043_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_personalize_admin_less_2 import * diff --git a/tests/010_kdfnone/test_044_adminless_kdfnone.py b/tests/010_kdfnone/test_044_adminless_kdfnone.py index a290b46..e4fce68 100644 --- a/tests/010_kdfnone/test_044_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_044_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_personalize_reset import * diff --git a/tests/010_kdfnone/test_045_adminless_kdfnone.py b/tests/010_kdfnone/test_045_adminless_kdfnone.py index ca02bfb..413c527 100644 --- a/tests/010_kdfnone/test_045_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_045_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_remove_keys import * diff --git a/tests/010_kdfnone/test_046_adminless_kdfnone.py b/tests/010_kdfnone/test_046_adminless_kdfnone.py index 5b0b496..3fdc611 100644 --- a/tests/010_kdfnone/test_046_adminless_kdfnone.py +++ b/tests/010_kdfnone/test_046_adminless_kdfnone.py @@ -21,5 +21,4 @@ along with this program. If not, see . """ from skip_if_kdfreq import * -from skip_gnuk_only_tests import * from card_test_reset_pw3 import * diff --git a/tests/010_kdfnone/test_047_adminless_upgrade_kdfnone.py b/tests/010_kdfnone/test_047_adminless_upgrade_kdfnone.py new file mode 100644 index 0000000..56c9d2b --- /dev/null +++ b/tests/010_kdfnone/test_047_adminless_upgrade_kdfnone.py @@ -0,0 +1,44 @@ +"""Explicitly opt an existing admin-full card into admin-less mode.""" + +from card_const import FACTORY_PASSPHRASE_PW1, FACTORY_PASSPHRASE_PW3 +from constants_for_test import PW1_TEST0, PW3_TEST0 +from skip_if_kdfreq import * + + +class Test_Card_Adminless_Upgrade: + def test_set_existing_admin_password(self, card): + assert card.change_passwd(3, FACTORY_PASSPHRASE_PW3, PW3_TEST0) + + def test_set_existing_user_password(self, card): + assert card.change_passwd(1, FACTORY_PASSPHRASE_PW1, PW1_TEST0) + + def test_enable_adminless(self, card): + assert card.change_passwd(3, PW3_TEST0, PW1_TEST0) + + def test_pw1_authorizes_admin_operation(self, card): + assert card.deauthenticate(1) + assert card.verify(1, PW1_TEST0) + assert card.cmd_put_data(0x00, 0x5e, b"adminless") + + def test_pw3_accepts_user_password(self, card): + assert card.verify(3, PW1_TEST0) + + def test_distinct_pw3_disables_adminless(self, card): + assert card.change_passwd(3, PW1_TEST0, PW3_TEST0) + assert card.deauthenticate(1) + assert card.deauthenticate(3) + assert card.verify(1, PW1_TEST0) + try: + card.cmd_put_data(0x00, 0x5e, b"adminfull") + except ValueError as e: + assert e.args[0] == "6982" + else: + raise AssertionError("PW1 unexpectedly retained admin authorization") + + def test_pw3_authorizes_admin_after_disable(self, card): + assert card.verify(3, PW3_TEST0) + assert card.cmd_put_data(0x00, 0x5e, b"adminfull") + + def test_restore_adminfull_factory_passwords(self, card): + assert card.change_passwd(3, PW3_TEST0, FACTORY_PASSPHRASE_PW3) + assert card.change_passwd(1, PW1_TEST0, FACTORY_PASSPHRASE_PW1) diff --git a/tests/030_kdfsingle/test_060_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_060_adminfull_kdfsingle.py index 419392d..60d2091 100644 --- a/tests/030_kdfsingle/test_060_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_060_adminfull_kdfsingle.py @@ -20,5 +20,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_kdf_single import * diff --git a/tests/030_kdfsingle/test_061_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_061_adminfull_kdfsingle.py index 3e7dbf2..ad82205 100644 --- a/tests/030_kdfsingle/test_061_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_061_adminfull_kdfsingle.py @@ -20,6 +20,5 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_personalize_card_1 import * from card_test_personalize_card_2 import * diff --git a/tests/030_kdfsingle/test_062_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_062_adminfull_kdfsingle.py index bbde30b..f68c5f8 100644 --- a/tests/030_kdfsingle/test_062_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_062_adminfull_kdfsingle.py @@ -20,5 +20,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_public_key_operations import * diff --git a/tests/030_kdfsingle/test_063_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_063_adminfull_kdfsingle.py index 7bc9e09..5f61b63 100644 --- a/tests/030_kdfsingle/test_063_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_063_adminfull_kdfsingle.py @@ -20,5 +20,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_ds_counter2 import * diff --git a/tests/030_kdfsingle/test_064_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_064_adminfull_kdfsingle.py index 13b9cfd..4b025cd 100644 --- a/tests/030_kdfsingle/test_064_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_064_adminfull_kdfsingle.py @@ -20,5 +20,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_personalize_reset import * diff --git a/tests/030_kdfsingle/test_065_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_065_adminfull_kdfsingle.py index 2a26aea..e754c3f 100644 --- a/tests/030_kdfsingle/test_065_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_065_adminfull_kdfsingle.py @@ -20,5 +20,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_remove_keys import * diff --git a/tests/030_kdfsingle/test_066_adminfull_kdfsingle.py b/tests/030_kdfsingle/test_066_adminfull_kdfsingle.py index 7de0685..dcc9065 100644 --- a/tests/030_kdfsingle/test_066_adminfull_kdfsingle.py +++ b/tests/030_kdfsingle/test_066_adminfull_kdfsingle.py @@ -20,5 +20,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from skip_gnuk_only_tests import * from card_test_reset_pw3 import * diff --git a/tests/030_kdfsingle/test_070_adminless_kdfsingle.py b/tests/030_kdfsingle/test_070_adminless_kdfsingle.py index f0d96e9..66d3e8d 100644 --- a/tests/030_kdfsingle/test_070_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_070_adminless_kdfsingle.py @@ -1,2 +1 @@ -from skip_gnuk_only_tests import * from card_test_personalize_admin_less_1 import * diff --git a/tests/030_kdfsingle/test_071_adminless_kdfsingle.py b/tests/030_kdfsingle/test_071_adminless_kdfsingle.py index 941ce0c..16dcb37 100644 --- a/tests/030_kdfsingle/test_071_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_071_adminless_kdfsingle.py @@ -1,2 +1 @@ -from skip_gnuk_only_tests import * from card_test_public_key_operations_alt import * diff --git a/tests/030_kdfsingle/test_072_adminless_kdfsingle.py b/tests/030_kdfsingle/test_072_adminless_kdfsingle.py index 6d65d38..26a65d1 100644 --- a/tests/030_kdfsingle/test_072_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_072_adminless_kdfsingle.py @@ -1,2 +1 @@ -from skip_gnuk_only_tests import * from card_test_ds_counter1 import * diff --git a/tests/030_kdfsingle/test_073_adminless_kdfsingle.py b/tests/030_kdfsingle/test_073_adminless_kdfsingle.py index 21a8768..aea3ecb 100644 --- a/tests/030_kdfsingle/test_073_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_073_adminless_kdfsingle.py @@ -1,2 +1 @@ -from skip_gnuk_only_tests import * from card_test_personalize_admin_less_2 import * diff --git a/tests/030_kdfsingle/test_074_adminless_kdfsingle.py b/tests/030_kdfsingle/test_074_adminless_kdfsingle.py index 0cd0538..dfacdeb 100644 --- a/tests/030_kdfsingle/test_074_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_074_adminless_kdfsingle.py @@ -1,2 +1 @@ -from skip_gnuk_only_tests import * from card_test_personalize_reset import * diff --git a/tests/030_kdfsingle/test_075_adminless_kdfsingle.py b/tests/030_kdfsingle/test_075_adminless_kdfsingle.py index f15e1eb..bcd8313 100644 --- a/tests/030_kdfsingle/test_075_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_075_adminless_kdfsingle.py @@ -1,2 +1 @@ -from skip_gnuk_only_tests import * from card_test_remove_keys import * diff --git a/tests/030_kdfsingle/test_076_adminless_kdfsingle.py b/tests/030_kdfsingle/test_076_adminless_kdfsingle.py index 86fcfb7..55ea971 100644 --- a/tests/030_kdfsingle/test_076_adminless_kdfsingle.py +++ b/tests/030_kdfsingle/test_076_adminless_kdfsingle.py @@ -1,2 +1,8 @@ -from skip_gnuk_only_tests import * from card_test_reset_pw3 import * +from card_const import FACTORY_PASSPHRASE_PW3 + + +def test_restore_adminfull_mode(card): + r = card.change_passwd(3, FACTORY_PASSPHRASE_PW3, + FACTORY_PASSPHRASE_PW3) + assert r diff --git a/tests/card_test_personalize_reset.py b/tests/card_test_personalize_reset.py index d488234..870f950 100644 --- a/tests/card_test_personalize_reset.py +++ b/tests/card_test_personalize_reset.py @@ -87,3 +87,13 @@ class Test_Personalize_Reset(object): else: r = card.cmd_put_data(0x00, 0xd3, b"") assert r + + def test_restore_reset_code(self, card): + # This module deletes D3 while resetting personalization. Restore it + # because the session-scoped card fixture is used by later suites. + assert card.verify(3, FACTORY_PASSPHRASE_PW3) + assert card.setup_reset_code(RESETCODE_TEST) + + def test_restore_pw1_status(self, card): + r = card.cmd_put_data(0x00, 0xc4, b"\x01") + assert r diff --git a/tests/card_test_reset_pw3.py b/tests/card_test_reset_pw3.py index a757f01..33d11ab 100644 --- a/tests/card_test_reset_pw3.py +++ b/tests/card_test_reset_pw3.py @@ -21,16 +21,11 @@ along with this program. If not, see . """ from card_const import * -import pytest class Test_Reset_PW3(object): - # Gnuk specific feature of clear PW3 def test_setup_pw3_null(self, card): - if card.is_gnuk: - r = card.change_passwd(3, FACTORY_PASSPHRASE_PW3, b'', kdf_change=-1) - assert r - else: - pytest.skip("Gnuk only feature of clearing PW3") + r = card.change_passwd(3, FACTORY_PASSPHRASE_PW3, b'', kdf_change=-1) + assert r def test_verify_pw3(self, card): v = card.verify(3, FACTORY_PASSPHRASE_PW3) diff --git a/tests/openpgp_card.py b/tests/openpgp_card.py index eadc706..b1da0a7 100644 --- a/tests/openpgp_card.py +++ b/tests/openpgp_card.py @@ -84,14 +84,13 @@ class OpenPGP_Card(object): self.kdf_supported = True def configure_kdf(self, kdf_config): + old_kdf_iters = self.__kdf_iters + old_kdf_salt_user = self.__kdf_salt_user + old_kdf_salt_admin = self.__kdf_salt_admin + self.kdf_data = kdf_config r = self.cmd_put_data(0x00, 0xf9, kdf_config) if self.kdf_data == b"" or self.kdf_data == b"\x81\x01\x00": - if not self.is_gnuk and not self.is_yubikey: - self.change_passwd(1, FACTORY_PASSPHRASE_PW1, - FACTORY_PASSPHRASE_PW1, -1) - self.change_passwd(3, FACTORY_PASSPHRASE_PW3, - FACTORY_PASSPHRASE_PW3, -1) self.__kdf_iters = None self.__kdf_salt_user = None self.__kdf_salt_reset = None @@ -102,11 +101,25 @@ class OpenPGP_Card(object): self.__kdf_salt_user = salt_user self.__kdf_salt_reset = salt_reset self.__kdf_salt_admin = salt_admin - if not self.is_gnuk and not self.is_yubikey: - self.change_passwd(1, FACTORY_PASSPHRASE_PW1, - FACTORY_PASSPHRASE_PW1, 1) - self.change_passwd(3, FACTORY_PASSPHRASE_PW3, - FACTORY_PASSPHRASE_PW3, 1) + + if not self.is_gnuk and not self.is_yubikey: + def kdf_value(pin, who, iterations, salt_user, salt_admin): + if not iterations: + return pin + salt = salt_admin if who == 3 and salt_admin else salt_user + return kdf_calc(pin, salt, iterations) + + old_pw1 = kdf_value(FACTORY_PASSPHRASE_PW1, 1, + old_kdf_iters, old_kdf_salt_user, old_kdf_salt_admin) + new_pw1 = kdf_value(FACTORY_PASSPHRASE_PW1, 1, + self.__kdf_iters, self.__kdf_salt_user, self.__kdf_salt_admin) + self.cmd_change_reference_data(1, old_pw1 + new_pw1) + + old_pw3 = kdf_value(FACTORY_PASSPHRASE_PW3, 3, + old_kdf_iters, old_kdf_salt_user, old_kdf_salt_admin) + new_pw3 = kdf_value(FACTORY_PASSPHRASE_PW3, 3, + self.__kdf_iters, self.__kdf_salt_user, self.__kdf_salt_admin) + self.cmd_change_reference_data(3, old_pw3 + new_pw3) return r def save_algo_attribute(self, keyno, attr): @@ -126,6 +139,15 @@ class OpenPGP_Card(object): else: return self.cmd_verify(who, passwd) + def deauthenticate(self, who): + cmd_data = iso7816_compose(0x20, 0xff, 0x80+who, b'') + sw = self.__reader.send_cmd(cmd_data) + if len(sw) != 2: + raise ValueError(sw) + if not (sw[0] == 0x90 and sw[1] == 0x00): + raise ValueError("%02x%02x" % (sw[0], sw[1])) + return True + # Higher layer CHANGE_PASSWD possibly using KDF Data Object # KDF_CHANGE: 0 no-change, -1 to be cleared, 1 to be configured def change_passwd(self, who, passwd_old, passwd_new, kdf_change=0):