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):