From 3f437db1c2c239b4d3d4e14538f072797d4e8113 Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Tue, 25 Aug 2026 14:31:17 +0200 Subject: [PATCH] Add Pico Vault to OpenPGP and PIV Signed-off-by: Pol Henarejos --- CMakeLists.txt | 1 + pico-keys-sdk | 2 +- src/openpgp/cmd_terminate_df.c | 5 + src/openpgp/files.c | 8 +- src/openpgp/files.h | 1 + src/openpgp/key_container.c | 6 + src/openpgp/object_provider.c | 34 ++ src/openpgp/object_provider.h | 4 + src/openpgp/openpgp.c | 4 +- src/openpgp/openpgp.h | 5 + src/openpgp/openpgp_vault.c | 557 +++++++++++++++++++++ src/openpgp/piv.c | 41 +- src/openpgp/vault.h | 45 ++ tests/openpgp/openpgp_key_container_test.c | 5 + tests/pico-openpgp/conftest.py | 21 + tests/pico-openpgp/test_080_vault.py | 364 ++++++++++++++ 16 files changed, 1092 insertions(+), 11 deletions(-) create mode 100644 src/openpgp/openpgp_vault.c create mode 100644 src/openpgp/vault.h create mode 100644 tests/pico-openpgp/conftest.py create mode 100644 tests/pico-openpgp/test_080_vault.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 59bc7df..2f3d2c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/openpgp.c ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/object_provider.c ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/key_container.c + ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/openpgp_vault.c ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/files.c ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/piv.c ${CMAKE_CURRENT_LIST_DIR}/src/openpgp/management.c diff --git a/pico-keys-sdk b/pico-keys-sdk index 9eedb0e..c791c1b 160000 --- a/pico-keys-sdk +++ b/pico-keys-sdk @@ -1 +1 @@ -Subproject commit 9eedb0ef8db79d29ab34e7d0f599219b813e91ea +Subproject commit c791c1be12a6355d46e4f7fba4ae323f8f1654ce diff --git a/src/openpgp/cmd_terminate_df.c b/src/openpgp/cmd_terminate_df.c index 4aab445..49c9de2 100644 --- a/src/openpgp/cmd_terminate_df.c +++ b/src/openpgp/cmd_terminate_df.c @@ -42,6 +42,11 @@ int cmd_terminate_df(void) { if (apdu.nc != 0) { return SW_WRONG_LENGTH(); } + /* The flash reset is global; do not erase a provisioned PIV application. */ + file_t *piv_dek = file_search_by_fid(EF_DEK_PWPIV, NULL, SPECIFY_EF); + if (piv_dek && file_has_data(piv_dek)) { + return SW_FUNC_NOT_SUPPORTED(); + } file_initialize_flash(true); scan_files_openpgp(); return SW_OK(); diff --git a/src/openpgp/files.c b/src/openpgp/files.c index 2a84add..38bff2c 100644 --- a/src/openpgp/files.c +++ b/src/openpgp/files.c @@ -495,10 +495,12 @@ file_entry_t file_entries[] = { /* 140 */ { .fid = EF_DEK_PWPIV, .parent = 0, .name = NULL, .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = ACL_NONE }, - - /* 141 */ { .fid = 0x0000, .parent = 0, .name = openpgp_aid, .type = FILE_TYPE_WORKING_EF, + /* 141 */ { .fid = EF_VAULT_KEY, .parent = 0, .name = NULL, + .type = FILE_TYPE_INTERNAL_EF | FILE_DATA_FLASH, .data = NULL, + .ef_structure = FILE_EF_TRANSPARENT, .acl = ACL_NONE }, + /* 142 */ { .fid = 0x0000, .parent = 0, .name = openpgp_aid, .type = FILE_TYPE_WORKING_EF, .data = NULL, .ef_structure = FILE_EF_TRANSPARENT, .acl = ACL_RO }, - /* 142 */ { .fid = 0x0000, .parent = 0xff, .name = NULL, .type = FILE_TYPE_NOT_KNOWN, .data = NULL, + /* 143 */ { .fid = 0x0000, .parent = 0xff, .name = NULL, .type = FILE_TYPE_NOT_KNOWN, .data = NULL, .ef_structure = 0, .acl = ACL_NONE } //end }; diff --git a/src/openpgp/files.h b/src/openpgp/files.h index a4cc774..5e9ee6d 100644 --- a/src/openpgp/files.h +++ b/src/openpgp/files.h @@ -40,6 +40,7 @@ #define EF_DEK_RC 0x109b #define EF_DEK_PW3 0x109c #define EF_DEK_PWPIV 0x109d +#define EF_VAULT_KEY 0xce03 #define EF_CH_1 0x1f21 #define EF_CH_2 0x1f22 #define EF_CH_3 0x1f23 diff --git a/src/openpgp/key_container.c b/src/openpgp/key_container.c index 1dc12b1..2c42334 100644 --- a/src/openpgp/key_container.c +++ b/src/openpgp/key_container.c @@ -305,8 +305,14 @@ int openpgp_key_container_store(uint16_t fid, const uint8_t *private_data, uint3 static bool openpgp_key_private_operation_authorized(uint16_t fid, uint16_t operation, bool internal_firmware) { if (openpgp_key_container_is_piv(fid)) { + if (operation == FILE_OBJECT_OPERATION_EXPORT) { + return internal_firmware && openpgp_vault_backup_authorized(OPENPGP_VAULT_APP_PIV); + } return piv_key_operation_authorized(operation, internal_firmware); } + if (operation == FILE_OBJECT_OPERATION_EXPORT) { + return openpgp_vault_backup_authorized(OPENPGP_VAULT_APP_OPENPGP); + } if (operation == FILE_OBJECT_OPERATION_UPDATE || operation == FILE_OBJECT_OPERATION_DELETE || operation == FILE_OBJECT_OPERATION_CHANGE_POLICY) { return has_pw3; } diff --git a/src/openpgp/object_provider.c b/src/openpgp/object_provider.c index 7e8fcf3..41b22b1 100644 --- a/src/openpgp/object_provider.c +++ b/src/openpgp/object_provider.c @@ -22,8 +22,10 @@ #include "openpgp.h" static file_object_crypto_provider_t openpgp_object_crypto_provider; +static file_object_crypto_provider_t openpgp_vault_object_crypto_provider; static file_object_crypto_provider_t openpgp_piv_object_crypto_provider; static bool openpgp_object_crypto_provider_initialized; +static bool openpgp_vault_object_crypto_provider_initialized; static bool openpgp_piv_object_crypto_provider_initialized; static int openpgp_object_root_load(void *ctx, uint8_t root[FILE_OBJECT_CRYPTO_ROOT_KEY_SIZE]) { @@ -74,6 +76,24 @@ static int openpgp_object_crypto_provider_init(void) { return r; } +static int openpgp_vault_object_crypto_provider_init(void) { + if (openpgp_vault_object_crypto_provider_initialized) { + return PICOKEYS_OK; + } + + const file_object_crypto_provider_config_t config = { + .namespace_id = OPENPGP_VAULT_OBJECT_NAMESPACE, + .load_root = openpgp_object_root_load, + .load_public_root = openpgp_object_public_root_load, + .identity_valid = openpgp_object_identity_valid + }; + int r = file_object_crypto_provider_init(&openpgp_vault_object_crypto_provider, &config); + if (r == PICOKEYS_OK) { + openpgp_vault_object_crypto_provider_initialized = true; + } + return r; +} + static int openpgp_piv_object_crypto_provider_init(void) { if (openpgp_piv_object_crypto_provider_initialized) { return PICOKEYS_OK; @@ -106,6 +126,20 @@ const file_object_record_protector_t *openpgp_object_record_protector(void) { return file_object_crypto_record_protector(&openpgp_object_crypto_provider); } +const file_object_authenticator_t *openpgp_vault_object_manifest_authenticator(void) { + if (openpgp_vault_object_crypto_provider_init() != PICOKEYS_OK) { + return NULL; + } + return file_object_crypto_manifest_authenticator(&openpgp_vault_object_crypto_provider); +} + +const file_object_record_protector_t *openpgp_vault_object_record_protector(void) { + if (openpgp_vault_object_crypto_provider_init() != PICOKEYS_OK) { + return NULL; + } + return file_object_crypto_record_protector(&openpgp_vault_object_crypto_provider); +} + const file_object_authenticator_t *openpgp_piv_object_manifest_authenticator(void) { if (openpgp_piv_object_crypto_provider_init() != PICOKEYS_OK) { return NULL; diff --git a/src/openpgp/object_provider.h b/src/openpgp/object_provider.h index 16f39b5..7e9d80b 100644 --- a/src/openpgp/object_provider.h +++ b/src/openpgp/object_provider.h @@ -21,9 +21,13 @@ #include "object_container.h" #define OPENPGP_OBJECT_NAMESPACE 0x0005u +/* The SDK vault layout and FIDO vault use namespace 0x0002. */ +#define OPENPGP_VAULT_OBJECT_NAMESPACE 0x0002u const file_object_authenticator_t *openpgp_object_manifest_authenticator(void); const file_object_record_protector_t *openpgp_object_record_protector(void); +const file_object_authenticator_t *openpgp_vault_object_manifest_authenticator(void); +const file_object_record_protector_t *openpgp_vault_object_record_protector(void); const file_object_authenticator_t *openpgp_piv_object_manifest_authenticator(void); const file_object_record_protector_t *openpgp_piv_object_record_protector(void); diff --git a/src/openpgp/openpgp.c b/src/openpgp/openpgp.c index 73d1e72..9894046 100644 --- a/src/openpgp/openpgp.c +++ b/src/openpgp/openpgp.c @@ -532,8 +532,6 @@ void release_dek(void) { memset(dek, 0, sizeof(dek)); } -extern bool has_pwpiv; -extern uint8_t session_pwpiv[32]; #define PIN_TXN_PAYLOAD_SIZE (2u + 34u + DEK_FILE_SIZE) static const file_object_txn_layout_t pin_txn_layout_pw1 = { @@ -1625,6 +1623,7 @@ int ecdsa_sign(mbedtls_ecp_keypair *ctx, const uint8_t *data, size_t data_len, u #define INS_IMPORT_DATA 0xDB #define INS_TERMINATE_DF 0xE6 #define INS_VERSION 0xF1 +#define INS_VAULT 0xF2 static const cmd_t cmds[] = { { INS_GET_DATA, cmd_get_data }, @@ -1645,6 +1644,7 @@ static const cmd_t cmds[] = { { INS_SELECT_DATA, cmd_select_data }, { INS_GET_NEXT_DATA, cmd_get_next_data }, { INS_GET_BULK_DATA, cmd_get_bulk_data }, + { INS_VAULT, cmd_openpgp_vault }, { 0x00, NULL } }; diff --git a/src/openpgp/openpgp.h b/src/openpgp/openpgp.h index 7a0d593..ec8010e 100644 --- a/src/openpgp/openpgp.h +++ b/src/openpgp/openpgp.h @@ -29,6 +29,7 @@ #include "mbedtls/ecdsa.h" #include "crypto_utils.h" #include "files.h" +#include "vault.h" extern bool has_pw1; extern bool has_pw2; @@ -38,6 +39,8 @@ extern uint8_t session_pw1[32]; extern uint8_t session_rc[32]; extern uint8_t session_pw3[32]; extern uint8_t dek[IV_SIZE + 32]; +extern bool has_pwpiv; +extern uint8_t session_pwpiv[32]; extern int store_keys(void *key_ctx, int type, uint16_t key_id, bool use_kek); extern int store_keypair(void *key_ctx, int type, uint16_t key_id, const uint8_t *public_data, size_t public_size); @@ -107,6 +110,8 @@ int cmd_pso(void); int cmd_keypair_gen(void); int cmd_reset_retry(void); int cmd_get_bulk_data(void); +int cmd_openpgp_vault(void); +int cmd_piv_vault(void); #define DEK_SIZE (IV_SIZE + 32) #define DEK_AAD_SIZE (PIN_KDF_SIZE(DEK_SIZE)) diff --git a/src/openpgp/openpgp_vault.c b/src/openpgp/openpgp_vault.c new file mode 100644 index 0000000..79ec71f --- /dev/null +++ b/src/openpgp/openpgp_vault.c @@ -0,0 +1,557 @@ +/* + * This file is part of the Pico OpenPGP distribution (https://github.com/polhenarejos/pico-openpgp). + * Copyright (c) 2022 Pol Henarejos. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "vault.h" + +#include + +#include "files.h" +#include "key_container.h" +#include "mbedtls/constant_time.h" +#include "object_provider.h" +#include "openpgp.h" +#include "random.h" +#include "serial.h" + +#define OPENPGP_VAULT_SUBCOMMAND_STATUS 0x01 +#define OPENPGP_VAULT_SUBCOMMAND_START_ENROLLMENT 0x02 +#define OPENPGP_VAULT_SUBCOMMAND_FINISH_ENROLLMENT 0x03 +#define OPENPGP_VAULT_SUBCOMMAND_EXPORT 0x04 +#define OPENPGP_VAULT_SUBCOMMAND_IMPORT 0x05 +#define OPENPGP_VAULT_SUBCOMMAND_UNENROLL 0x06 + +#define OPENPGP_VAULT_BLOB_HEADER_SIZE 86u +#define OPENPGP_VAULT_BLOB_SERIAL_MAX 16u +#define OPENPGP_VAULT_BLOB_SERIAL_LENGTH_OFFSET 68u +#define OPENPGP_VAULT_BLOB_SERIAL_OFFSET 69u +#define OPENPGP_VAULT_BLOB_ALGORITHM_OFFSET 85u +#define OPENPGP_VAULT_PLAIN_MAX OPENPGP_MAX_RESPONSE_SIZE +#define OPENPGP_VAULT_BLOB_MAX OPENPGP_MAX_RESPONSE_SIZE + +#define OPENPGP_VAULT_HANDLE_SIGNATURE 0x01 +#define OPENPGP_VAULT_HANDLE_DECRYPTION 0x02 +#define OPENPGP_VAULT_HANDLE_AUTHENTICATION 0x03 +#define OPENPGP_VAULT_HANDLE_AES 0x04 + +static const uint8_t openpgp_vault_blob_magic[] = { 'P', 'K', 'V', 1 }; + +typedef struct openpgp_vault_plaintext { + uint8_t app; + uint16_t fid; + uint8_t private_data[OPENPGP_MAX_OBJECT_SIZE]; + size_t private_len; + uint8_t public_data[OPENPGP_MAX_OBJECT_SIZE]; + size_t public_len; +} openpgp_vault_plaintext_t; + +static file_t *openpgp_vault_file(void) { + return file_search_by_fid(EF_VAULT_KEY, NULL, SPECIFY_EF); +} + +static int openpgp_vault_sdk_init(void) { + file_object_container_crypto_t primary = { + .auth = openpgp_vault_object_manifest_authenticator(), + .protector = openpgp_vault_object_record_protector() + }; + if (!primary.auth || !primary.protector) { + return PICOKEYS_EXEC_ERROR; + } + return picokeys_vault_init(&primary, NULL, openpgp_vault_file(), NULL); +} + +static bool openpgp_vault_app_valid(openpgp_vault_app_t app) { + return app < OPENPGP_VAULT_APP_COUNT && app != OPENPGP_VAULT_APP_FIDO; +} + +static bool openpgp_vault_app_authenticated(openpgp_vault_app_t app) { + return app == OPENPGP_VAULT_APP_OPENPGP ? has_pw3 : app == OPENPGP_VAULT_APP_PIV ? has_pwpiv : false; +} + +static int openpgp_vault_load_root(openpgp_vault_app_t app, uint8_t root[OPENPGP_VAULT_KEY_SIZE]) { + if (!root || !openpgp_vault_app_valid(app) || !openpgp_vault_app_authenticated(app)) { + return PICOKEYS_NO_LOGIN; + } + int ret = load_dek(); + if (ret == PICOKEYS_OK) { + memcpy(root, dek + IV_SIZE, OPENPGP_VAULT_KEY_SIZE); + } + release_dek(); + return ret; +} + +bool openpgp_vault_is_enrolled(void) { + return openpgp_vault_wrapper_available(OPENPGP_VAULT_APP_OPENPGP) || openpgp_vault_wrapper_available(OPENPGP_VAULT_APP_PIV); +} + +bool openpgp_vault_wrapper_available(openpgp_vault_app_t app) { + if (!openpgp_vault_app_valid(app) || openpgp_vault_sdk_init() != PICOKEYS_OK) { + return false; + } + return openpgp_vault_app_authenticated(app) && picokeys_vault_wrap_available(app); +} + +int openpgp_vault_load_kvault(openpgp_vault_app_t app, uint8_t kvault[OPENPGP_VAULT_KEY_SIZE]) { + if (!kvault || !openpgp_vault_app_valid(app)) { + return PICOKEYS_ERR_NULL_PARAM; + } + if (openpgp_vault_sdk_init() != PICOKEYS_OK) { + return PICOKEYS_EXEC_ERROR; + } + uint8_t root[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + int ret = openpgp_vault_load_root(app, root); + if (ret == PICOKEYS_OK) { + ret = picokeys_vault_get_kvault(app, root, kvault); + } + mbedtls_platform_zeroize(root, sizeof(root)); + return ret; +} + +int openpgp_vault_store_kvault(openpgp_vault_app_t app, const uint8_t kvault[OPENPGP_VAULT_KEY_SIZE]) { + if (!kvault || !openpgp_vault_app_valid(app)) { + return PICOKEYS_ERR_NULL_PARAM; + } + if (openpgp_vault_sdk_init() != PICOKEYS_OK) { + return PICOKEYS_EXEC_ERROR; + } + uint8_t root[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + int ret = openpgp_vault_load_root(app, root); + if (ret == PICOKEYS_OK) { + ret = picokeys_vault_set_kvault(kvault, root, app); + } + mbedtls_platform_zeroize(root, sizeof(root)); + return ret; +} + +int openpgp_vault_clear_wrappers(void) { + if (openpgp_vault_sdk_init() != PICOKEYS_OK) { + return PICOKEYS_EXEC_ERROR; + } + int ret = PICOKEYS_OK; + for (openpgp_vault_app_t app = OPENPGP_VAULT_APP_OPENPGP; app < OPENPGP_VAULT_APP_COUNT; app++) { + if (openpgp_vault_app_authenticated(app)) { + int current = picokeys_vault_delete_kvault(app); + if (current != PICOKEYS_OK && current != PICOKEYS_ERR_FILE_NOT_FOUND) { + ret = current; + } + } + } + return ret; +} + +bool openpgp_vault_backup_authorized(openpgp_vault_app_t app) { + uint8_t kvault[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + int ret = openpgp_vault_load_kvault(app, kvault); + mbedtls_platform_zeroize(kvault, sizeof(kvault)); + return ret == PICOKEYS_OK; +} + +static int openpgp_vault_handle_fid(openpgp_vault_app_t app, uint8_t handle, uint16_t *fid) { + if (!fid) { + return PICOKEYS_ERR_NULL_PARAM; + } + if (app == OPENPGP_VAULT_APP_OPENPGP) { + if (handle == OPENPGP_VAULT_HANDLE_SIGNATURE) { + *fid = EF_PK_SIG; + } + else if (handle == OPENPGP_VAULT_HANDLE_DECRYPTION) { + *fid = EF_PK_DEC; + } + else if (handle == OPENPGP_VAULT_HANDLE_AUTHENTICATION) { + *fid = EF_PK_AUT; + } + else if (handle == OPENPGP_VAULT_HANDLE_AES) { + *fid = EF_AES_KEY; + } + else { + return PICOKEYS_WRONG_DATA; + } + return PICOKEYS_OK; + } + if (app != OPENPGP_VAULT_APP_PIV || handle == EF_PIV_KEY_CARDMGM || handle == 0x80 || handle == 0x81) { + return PICOKEYS_WRONG_DATA; + } + if (handle == 0xF9) { + *fid = EF_PIV_KEY_ATTESTATION; + } + else if (handle == 0x93) { + *fid = EF_PIV_KEY_RETIRED18; + } + else { + *fid = handle; + } + return openpgp_key_container_supported(*fid) ? PICOKEYS_OK : PICOKEYS_WRONG_DATA; +} + +static void openpgp_vault_object_hash(openpgp_vault_app_t app, uint16_t fid, uint8_t hash[OPENPGP_VAULT_KEY_SIZE]) { + uint8_t identity[3] = { app, (uint8_t)(fid >> 8), (uint8_t)fid }; + hash256(CONST_BYTE_ARRAY(identity, sizeof(identity)), hash); +} + +static int openpgp_vault_encode_plaintext(openpgp_vault_app_t app, uint16_t fid, const uint8_t *private_data, size_t private_len, const uint8_t *public_data, size_t public_len, uint8_t *plain, size_t plain_capacity, size_t *plain_len) { + if (!plain || !plain_len || private_len > OPENPGP_MAX_OBJECT_SIZE || public_len > OPENPGP_MAX_OBJECT_SIZE || (!private_data && private_len > 0) || (!public_data && public_len > 0)) { + return PICOKEYS_ERR_NULL_PARAM; + } + size_t needed = 8u + private_len + public_len; + if (private_len > UINT16_MAX || public_len > UINT16_MAX || needed > plain_capacity) { + return PICOKEYS_ERR_NO_MEMORY; + } + plain[0] = 1; + plain[1] = app; + put_uint16_be(fid, plain + 2); + put_uint16_be((uint16_t)private_len, plain + 4); + memcpy(plain + 6, private_data, private_len); + put_uint16_be((uint16_t)public_len, plain + 6 + private_len); + memcpy(plain + 8 + private_len, public_data, public_len); + *plain_len = needed; + return PICOKEYS_OK; +} + +static int openpgp_vault_decode_plaintext(const uint8_t *plain, size_t plain_len, openpgp_vault_plaintext_t *output) { + if (!plain || !output) { + return PICOKEYS_ERR_NULL_PARAM; + } + if (plain_len < 8u || plain[0] != 1 || plain[1] >= OPENPGP_VAULT_APP_COUNT) { + return PICOKEYS_WRONG_DATA; + } + uint16_t private_len = get_uint16_be(plain + 4); + if ((size_t)private_len + 8u > plain_len) { + return PICOKEYS_WRONG_DATA; + } + size_t public_length_offset = 6u + private_len; + if (public_length_offset + 2u > plain_len) { + return PICOKEYS_WRONG_DATA; + } + uint16_t public_len = get_uint16_be(plain + public_length_offset); + if (public_length_offset + 2u + public_len != plain_len || private_len > OPENPGP_MAX_OBJECT_SIZE || public_len > OPENPGP_MAX_OBJECT_SIZE || private_len == 0) { + return PICOKEYS_WRONG_DATA; + } + memset(output, 0, sizeof(*output)); + output->app = plain[1]; + output->fid = get_uint16_be(plain + 2); + output->private_len = private_len; + output->public_len = public_len; + memcpy(output->private_data, plain + 6, private_len); + memcpy(output->public_data, plain + public_length_offset + 2, public_len); + return PICOKEYS_OK; +} + +static int openpgp_vault_load_keys(openpgp_vault_app_t app, uint8_t kvault[OPENPGP_VAULT_KEY_SIZE], uint8_t vault_id[OPENPGP_VAULT_KEY_SIZE], const uint8_t object_hash[OPENPGP_VAULT_KEY_SIZE], uint8_t algorithm, uint8_t keys[2][OPENPGP_VAULT_KEY_SIZE]) { + if (openpgp_vault_load_kvault(app, kvault) != PICOKEYS_OK || picokeys_vault_hash_kvault(kvault, vault_id) != PICOKEYS_OK) { + return PICOKEYS_NO_LOGIN; + } + size_t layers = picokeys_vault_algorithm_layers(algorithm); + for (size_t layer = 0; layer < layers; layer++) { + if (picokeys_vault_layer_key(kvault, vault_id, object_hash, algorithm, (uint8_t)layer, keys[layer]) != PICOKEYS_OK) { + return PICOKEYS_EXEC_ERROR; + } + } + return PICOKEYS_OK; +} + +static int openpgp_vault_export(openpgp_vault_app_t app, uint16_t fid, uint8_t algorithm, uint8_t *blob, size_t blob_capacity, size_t *blob_len) { + if (!blob || !blob_len || !picokeys_vault_algorithm_valid(algorithm)) { + return PICOKEYS_WRONG_DATA; + } + uint8_t private_data[OPENPGP_MAX_OBJECT_SIZE] = { 0 }; + uint8_t public_data[OPENPGP_MAX_OBJECT_SIZE] = { 0 }; + byte_buffer_t private_output = BYTE_BUFFER(private_data, sizeof(private_data)); + byte_buffer_t public_output = BYTE_BUFFER(public_data, sizeof(public_data)); + int ret = openpgp_key_container_read_private(fid, FILE_OBJECT_OPERATION_EXPORT, true, &private_output); + if (ret != PICOKEYS_OK) { + goto cleanup; + } + ret = openpgp_key_container_read_public(fid, &public_output); + if (ret == PICOKEYS_ERR_FILE_NOT_FOUND) { + ret = PICOKEYS_OK; + } + if (ret != PICOKEYS_OK) { + goto cleanup; + } + uint8_t plain[OPENPGP_VAULT_PLAIN_MAX] = { 0 }; + size_t plain_len = 0; + ret = openpgp_vault_encode_plaintext(app, fid, private_data, private_output.len, public_data, public_output.len, plain, sizeof(plain), &plain_len); + if (ret != PICOKEYS_OK) { + goto cleanup_plain; + } + size_t layers = picokeys_vault_algorithm_layers(algorithm); + size_t nonce_len = layers * PICOKEYS_VAULT_BLOB_NONCE_SIZE; + size_t total_len = OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len + plain_len + layers * PICOKEYS_VAULT_BLOB_TAG_SIZE; + if (total_len > blob_capacity) { + ret = PICOKEYS_ERR_NO_MEMORY; + goto cleanup_plain; + } + uint8_t kvault[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t vault_id[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t object_hash[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t keys[2][OPENPGP_VAULT_KEY_SIZE] = { 0 }; + openpgp_vault_object_hash(app, fid, object_hash); + ret = openpgp_vault_load_keys(app, kvault, vault_id, object_hash, algorithm, keys); + if (ret != PICOKEYS_OK) { + goto cleanup_keys; + } + memcpy(blob, openpgp_vault_blob_magic, sizeof(openpgp_vault_blob_magic)); + memcpy(blob + 4, vault_id, sizeof(vault_id)); + memcpy(blob + 36, object_hash, sizeof(object_hash)); + blob[OPENPGP_VAULT_BLOB_SERIAL_LENGTH_OFFSET] = sizeof(pico_serial.id) <= OPENPGP_VAULT_BLOB_SERIAL_MAX ? sizeof(pico_serial.id) : OPENPGP_VAULT_BLOB_SERIAL_MAX; + memcpy(blob + OPENPGP_VAULT_BLOB_SERIAL_OFFSET, pico_serial.id, blob[OPENPGP_VAULT_BLOB_SERIAL_LENGTH_OFFSET]); + blob[OPENPGP_VAULT_BLOB_ALGORITHM_OFFSET] = algorithm; + random_fill_buffer(BYTE_ARRAY(blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, nonce_len)); + if (layers == 1) { + ret = picokeys_vault_encrypt_layer(algorithm, keys[0], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, plain, plain_len, blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len, blob + total_len - PICOKEYS_VAULT_BLOB_TAG_SIZE); + } + else { + uint8_t intermediate[OPENPGP_VAULT_PLAIN_MAX + PICOKEYS_VAULT_BLOB_TAG_SIZE] = { 0 }; + uint8_t first[PICOKEYS_VAULT_BLOB_TAG_SIZE]; + ret = picokeys_vault_encrypt_layer(picokeys_vault_algorithm_layer(algorithm, 0), keys[0], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, plain, plain_len, intermediate, first); + if (ret == PICOKEYS_OK) { + ret = picokeys_vault_encrypt_layer(picokeys_vault_algorithm_layer(algorithm, 1), keys[1], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + PICOKEYS_VAULT_BLOB_NONCE_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, intermediate, plain_len + PICOKEYS_VAULT_BLOB_TAG_SIZE, blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len, blob + total_len - PICOKEYS_VAULT_BLOB_TAG_SIZE); + } + mbedtls_platform_zeroize(intermediate, sizeof(intermediate)); + mbedtls_platform_zeroize(first, sizeof(first)); + } + if (ret == PICOKEYS_OK) { + *blob_len = total_len; + } +cleanup_keys: + mbedtls_platform_zeroize(keys, sizeof(keys)); + mbedtls_platform_zeroize(object_hash, sizeof(object_hash)); + mbedtls_platform_zeroize(vault_id, sizeof(vault_id)); + mbedtls_platform_zeroize(kvault, sizeof(kvault)); +cleanup_plain: + mbedtls_platform_zeroize(plain, sizeof(plain)); +cleanup: + mbedtls_platform_zeroize(private_data, sizeof(private_data)); + mbedtls_platform_zeroize(public_data, sizeof(public_data)); + return ret; +} + +static int openpgp_vault_import(openpgp_vault_app_t app, uint16_t target_fid, const uint8_t *blob, size_t blob_len) { + if (!blob || blob_len < OPENPGP_VAULT_BLOB_HEADER_SIZE + PICOKEYS_VAULT_BLOB_NONCE_SIZE + PICOKEYS_VAULT_BLOB_TAG_SIZE || memcmp(blob, openpgp_vault_blob_magic, sizeof(openpgp_vault_blob_magic)) != 0 || blob[OPENPGP_VAULT_BLOB_SERIAL_LENGTH_OFFSET] > OPENPGP_VAULT_BLOB_SERIAL_MAX) { + return PICOKEYS_WRONG_DATA; + } + uint8_t algorithm = blob[OPENPGP_VAULT_BLOB_ALGORITHM_OFFSET]; + if (!picokeys_vault_algorithm_valid(algorithm)) { + return PICOKEYS_WRONG_DATA; + } + size_t layers = picokeys_vault_algorithm_layers(algorithm); + size_t nonce_len = layers * PICOKEYS_VAULT_BLOB_NONCE_SIZE; + if (blob_len < OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len + layers * PICOKEYS_VAULT_BLOB_TAG_SIZE) { + return PICOKEYS_WRONG_LENGTH; + } + size_t encrypted_len = blob_len - OPENPGP_VAULT_BLOB_HEADER_SIZE - nonce_len; + size_t plain_len = encrypted_len - layers * PICOKEYS_VAULT_BLOB_TAG_SIZE; + if (plain_len == 0 || plain_len > OPENPGP_VAULT_PLAIN_MAX) { + return PICOKEYS_WRONG_LENGTH; + } + uint8_t kvault[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t vault_id[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t object_hash[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t keys[2][OPENPGP_VAULT_KEY_SIZE] = { 0 }; + memcpy(object_hash, blob + 36, sizeof(object_hash)); + int ret = openpgp_vault_load_keys(app, kvault, vault_id, object_hash, algorithm, keys); + if (ret != PICOKEYS_OK) { + goto cleanup; + } + if (mbedtls_ct_memcmp(vault_id, blob + 4, sizeof(vault_id)) != 0) { + ret = PICOKEYS_VERIFICATION_FAILED; + goto cleanup; + } + uint8_t plain[OPENPGP_VAULT_PLAIN_MAX] = { 0 }; + if (layers == 1) { + ret = picokeys_vault_decrypt_layer(algorithm, keys[0], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len, plain_len, blob + blob_len - PICOKEYS_VAULT_BLOB_TAG_SIZE, plain); + } + else { + uint8_t intermediate[OPENPGP_VAULT_PLAIN_MAX + PICOKEYS_VAULT_BLOB_TAG_SIZE] = { 0 }; + ret = picokeys_vault_decrypt_layer(picokeys_vault_algorithm_layer(algorithm, 1), keys[1], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + PICOKEYS_VAULT_BLOB_NONCE_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len, plain_len + PICOKEYS_VAULT_BLOB_TAG_SIZE, blob + blob_len - PICOKEYS_VAULT_BLOB_TAG_SIZE, intermediate); + if (ret == PICOKEYS_OK) { + ret = picokeys_vault_decrypt_layer(picokeys_vault_algorithm_layer(algorithm, 0), keys[0], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, intermediate, plain_len, intermediate + plain_len, plain); + } + mbedtls_platform_zeroize(intermediate, sizeof(intermediate)); + } + if (ret != PICOKEYS_OK) { + ret = PICOKEYS_VERIFICATION_FAILED; + mbedtls_platform_zeroize(plain, sizeof(plain)); + goto cleanup; + } + openpgp_vault_plaintext_t decoded; + ret = openpgp_vault_decode_plaintext(plain, plain_len, &decoded); + if (ret == PICOKEYS_OK && decoded.app != app) { + ret = PICOKEYS_WRONG_DATA; + } + if (ret == PICOKEYS_OK) { + uint8_t expected_hash[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + openpgp_vault_object_hash(decoded.app, decoded.fid, expected_hash); + if (mbedtls_ct_memcmp(expected_hash, blob + 36, sizeof(expected_hash)) != 0) { + ret = PICOKEYS_VERIFICATION_FAILED; + } + mbedtls_platform_zeroize(expected_hash, sizeof(expected_hash)); + } + if (ret == PICOKEYS_OK) { + ret = openpgp_key_container_store(target_fid, decoded.private_data, decoded.private_len, decoded.public_len ? decoded.public_data : NULL, decoded.public_len, true); + } + mbedtls_platform_zeroize(&decoded, sizeof(decoded)); + mbedtls_platform_zeroize(plain, sizeof(plain)); +cleanup: + mbedtls_platform_zeroize(keys, sizeof(keys)); + mbedtls_platform_zeroize(object_hash, sizeof(object_hash)); + mbedtls_platform_zeroize(vault_id, sizeof(vault_id)); + mbedtls_platform_zeroize(kvault, sizeof(kvault)); + return ret; +} + +static int openpgp_vault_get_label(uint8_t *label, size_t capacity, size_t *length) { + if (!label || !length || openpgp_vault_sdk_init() != PICOKEYS_OK) { + return PICOKEYS_ERR_NULL_PARAM; + } + byte_buffer_t output = BYTE_BUFFER(label, capacity); + int ret = picokeys_vault_get_label(&output); + *length = output.len; + return ret; +} + +int openpgp_vault_command(openpgp_vault_app_t app) { + if (!openpgp_vault_app_valid(app)) { + return SW_INCORRECT_P1P2(); + } + if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_STATUS) { + if (P2(apdu) != 0 || apdu.nc != 0) { + return SW_INCORRECT_P1P2(); + } + uint8_t kvault[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t vault_id[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t label[64] = { 0 }; + size_t label_len = 0; + bool stored = openpgp_vault_wrapper_available(app); + bool enrolled = stored && openpgp_vault_load_kvault(app, kvault) == PICOKEYS_OK && picokeys_vault_hash_kvault(kvault, vault_id) == PICOKEYS_OK; + if (stored) { + openpgp_vault_get_label(label, sizeof(label), &label_len); + } + res_APDU[0] = 1; + res_APDU[1] = stored; + res_APDU[2] = picokeys_vault_enrollment_button_ready(); + res_APDU[3] = enrolled ? sizeof(vault_id) : 0; + memcpy(res_APDU + 4, vault_id, sizeof(vault_id)); + res_APDU[36] = label_len; + memcpy(res_APDU + 37, label, label_len); + res_APDU_size = 37 + label_len; + mbedtls_platform_zeroize(kvault, sizeof(kvault)); + mbedtls_platform_zeroize(vault_id, sizeof(vault_id)); + mbedtls_platform_zeroize(label, sizeof(label)); + return SW_OK(); + } + if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_START_ENROLLMENT) { + if (P2(apdu) != 0 || apdu.nc != 0) { + return SW_INCORRECT_P1P2(); + } + if (!openpgp_vault_app_authenticated(app)) { + return SW_SECURITY_STATUS_NOT_SATISFIED(); + } + if (!picokeys_vault_enrollment_button_ready()) { + return SW_CONDITIONS_NOT_SATISFIED(); + } + uint8_t public_key[PICOKEYS_VAULT_X448_BYTES] = { 0 }; + uint8_t challenge[PICOKEYS_VAULT_ENROLL_CHALLENGE_BYTES] = { 0 }; + if (picokeys_vault_enrollment_start(public_key, challenge) != PICOKEYS_OK) { + return SW_EXEC_ERROR(); + } + memcpy(res_APDU, public_key, sizeof(public_key)); + memcpy(res_APDU + sizeof(public_key), challenge, sizeof(challenge)); + res_APDU_size = sizeof(public_key) + sizeof(challenge); + return SW_OK(); + } + if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_FINISH_ENROLLMENT) { + if (P2(apdu) != 0 || apdu.nc == 0) { + return SW_INCORRECT_P1P2(); + } + if (!openpgp_vault_app_authenticated(app)) { + return SW_SECURITY_STATUS_NOT_SATISFIED(); + } + uint8_t kvault[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + uint8_t label[64] = { 0 }; + size_t label_len = 0; + int ret = picokeys_vault_enrollment_decode(apdu.data, apdu.nc, kvault, label, sizeof(label), &label_len); + if (ret == PICOKEYS_OK) { + ret = openpgp_vault_store_kvault(app, kvault); + } + if (ret == PICOKEYS_OK) { + ret = openpgp_vault_sdk_init(); + } + if (ret == PICOKEYS_OK) { + ret = picokeys_vault_set_label(CONST_BYTE_ARRAY(label, label_len)); + } + uint8_t vault_id[OPENPGP_VAULT_KEY_SIZE] = { 0 }; + if (ret == PICOKEYS_OK) { + ret = picokeys_vault_hash_kvault(kvault, vault_id); + } + mbedtls_platform_zeroize(kvault, sizeof(kvault)); + mbedtls_platform_zeroize(label, sizeof(label)); + if (ret != PICOKEYS_OK) { + picokeys_vault_enrollment_reset(); + return SW_DATA_INVALID(); + } + memcpy(res_APDU, vault_id, sizeof(vault_id)); + res_APDU_size = sizeof(vault_id); + mbedtls_platform_zeroize(vault_id, sizeof(vault_id)); + return SW_OK(); + } + if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_EXPORT) { + uint16_t fid = 0; + if (apdu.nc > 1 || openpgp_vault_handle_fid(app, P2(apdu), &fid) != PICOKEYS_OK) { + return SW_INCORRECT_P1P2(); + } + uint8_t algorithm = apdu.nc == 1 ? apdu.data[0] : PICOKEYS_VAULT_ALGORITHM_CHACHAPOLY; + uint8_t blob[OPENPGP_VAULT_BLOB_MAX] = { 0 }; + size_t blob_len = 0; + int ret = openpgp_vault_export(app, fid, algorithm, blob, sizeof(blob), &blob_len); + if (ret != PICOKEYS_OK) { + mbedtls_platform_zeroize(blob, sizeof(blob)); + return ret == PICOKEYS_NO_LOGIN ? SW_SECURITY_STATUS_NOT_SATISFIED() : ret == PICOKEYS_ERR_NO_MEMORY ? SW_MEMORY_FAILURE() : SW_REFERENCE_NOT_FOUND(); + } + memcpy(res_APDU, blob, blob_len); + res_APDU_size = blob_len; + mbedtls_platform_zeroize(blob, sizeof(blob)); + return SW_OK(); + } + if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_IMPORT) { + uint16_t fid = 0; + if (apdu.nc == 0 || openpgp_vault_handle_fid(app, P2(apdu), &fid) != PICOKEYS_OK) { + return SW_INCORRECT_P1P2(); + } + int ret = openpgp_vault_import(app, fid, apdu.data, apdu.nc); + return ret == PICOKEYS_OK ? SW_OK() : ret == PICOKEYS_NO_LOGIN ? SW_SECURITY_STATUS_NOT_SATISFIED() : SW_DATA_INVALID(); + } + if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_UNENROLL) { + if (P2(apdu) != 0 || apdu.nc != 0) { + return SW_INCORRECT_P1P2(); + } + if (!openpgp_vault_app_authenticated(app) || openpgp_vault_sdk_init() != PICOKEYS_OK) { + return SW_SECURITY_STATUS_NOT_SATISFIED(); + } + int ret = picokeys_vault_delete_kvault(app); + return ret == PICOKEYS_OK || ret == PICOKEYS_ERR_FILE_NOT_FOUND ? SW_OK() : SW_EXEC_ERROR(); + } + return SW_INCORRECT_P1P2(); +} + +int cmd_openpgp_vault(void) { + return openpgp_vault_command(OPENPGP_VAULT_APP_OPENPGP); +} + +int cmd_piv_vault(void) { + return openpgp_vault_command(OPENPGP_VAULT_APP_PIV); +} diff --git a/src/openpgp/piv.c b/src/openpgp/piv.c index 71a2004..8e7d4e9 100644 --- a/src/openpgp/piv.c +++ b/src/openpgp/piv.c @@ -36,6 +36,7 @@ #include "key_container.h" #include "openpgp.h" #include "button.h" +#include "usb.h" #define PIV_ALGO_3DES 0x03 #define PIV_ALGO_AES128 0x08 @@ -75,6 +76,20 @@ #define IS_ACTIVE(x) ((x) >= EF_PIV_KEY_AUTHENTICATION && (x) <= EF_PIV_KEY_CARDAUTH) #define IS_KEY(x) ((IS_ACTIVE((x))) || (IS_RETIRED((x)))) +#ifndef ENABLE_EMULATION +extern void execute_tasks(void); + +static bool piv_button_wait(void) { + uint32_t event = 0; + button_wait_start(); + while (is_req_button_pending()) { + execute_tasks(); + } + queue_remove_blocking(&usb_to_card_q, &event); + return event != EV_BUTTON_PRESSED; +} +#endif + static size_t piv_rsa_modulus_size(uint8_t algo) { if (algo == PIV_ALGO_RSA1024) { return 128; @@ -900,7 +915,7 @@ static int cmd_authenticate(void) { return SW_SECURITY_STATUS_NOT_SATISFIED(); } #ifndef ENABLE_EMULATION - if (meta[2] != TOUCHPOLICY_NEVER && button_wait()) { + if (meta[2] != TOUCHPOLICY_NEVER && piv_button_wait()) { return SW_SECURITY_STATUS_NOT_SATISFIED(); } #endif @@ -1077,12 +1092,19 @@ static int cmd_asym_keygen(void) { make_rsa_response(&rsa); uint8_t cert[2048]; r = x509_create_cert(&rsa, a80.data[0], key_ref, false, cert, sizeof(cert)); + if (r <= 0) { + mbedtls_rsa_free(&rsa); + return SW_EXEC_ERROR(); + } file_t *ef = file_search_by_fid(key_cert, NULL, SPECIFY_ANY); - file_put_data(ef, CONST_BYTE_ARRAY(cert + sizeof(cert) - r, r)); + if (!ef || file_put_data(ef, CONST_BYTE_ARRAY(cert + sizeof(cert) - r, r)) != PICOKEYS_OK) { + mbedtls_rsa_free(&rsa); + return SW_EXEC_ERROR(); + } r = store_keys(&rsa, ALGO_RSA, key_ref == 0x93 ? EF_PIV_KEY_RETIRED18 : key_ref, false); mbedtls_rsa_free(&rsa); if (r != PICOKEYS_OK) { - return SW_EXEC_ERROR(); + return r == PICOKEYS_ERR_NO_MEMORY ? SW_FILE_FULL() : SW_EXEC_ERROR(); } } else if (a80.data[0] == PIV_ALGO_ECCP256 || a80.data[0] == PIV_ALGO_ECCP384) { @@ -1098,12 +1120,19 @@ static int cmd_asym_keygen(void) { make_ecdsa_response(&ecdsa); uint8_t cert[2048]; r = x509_create_cert(&ecdsa, a80.data[0], key_ref, false, cert, sizeof(cert)); + if (r <= 0) { + mbedtls_ecdsa_free(&ecdsa); + return SW_EXEC_ERROR(); + } file_t *ef = file_search_by_fid(key_cert, NULL, SPECIFY_ANY); - file_put_data(ef, CONST_BYTE_ARRAY(cert + sizeof(cert) - r, r)); + if (!ef || file_put_data(ef, CONST_BYTE_ARRAY(cert + sizeof(cert) - r, r)) != PICOKEYS_OK) { + mbedtls_ecdsa_free(&ecdsa); + return SW_EXEC_ERROR(); + } r = store_keys(&ecdsa, ALGO_ECDSA, key_ref == 0x93 ? EF_PIV_KEY_RETIRED18 : key_ref, false); mbedtls_ecdsa_free(&ecdsa); if (r != PICOKEYS_OK) { - return SW_EXEC_ERROR(); + return r == PICOKEYS_ERR_NO_MEMORY ? SW_FILE_FULL() : SW_EXEC_ERROR(); } } else if (a80.data[0] == PIV_ALGO_X25519) { @@ -1640,6 +1669,7 @@ static int cmd_import_asym(void) { #define INS_RESET 0xFB #define INS_ATTESTATION 0xF9 #define INS_IMPORT_ASYM 0xFE +#define INS_VAULT 0xF2 static const cmd_t cmds[] = { { INS_VERSION, cmd_version }, @@ -1659,6 +1689,7 @@ static const cmd_t cmds[] = { { INS_RESET, cmd_reset }, { INS_ATTESTATION, cmd_attestation }, { INS_IMPORT_ASYM, cmd_import_asym }, + { INS_VAULT, cmd_piv_vault }, { 0x00, 0x0 } }; diff --git a/src/openpgp/vault.h b/src/openpgp/vault.h new file mode 100644 index 0000000..f9234d5 --- /dev/null +++ b/src/openpgp/vault.h @@ -0,0 +1,45 @@ +/* + * This file is part of the Pico OpenPGP distribution (https://github.com/polhenarejos/pico-openpgp). + * Copyright (c) 2022 Pol Henarejos. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#ifndef _OPENPGP_VAULT_H_ +#define _OPENPGP_VAULT_H_ + +#include +#include +#include + +#include "crypto_utils.h" +#include "vault_container.h" + +#define OPENPGP_VAULT_KEY_SIZE PICOKEYS_VAULT_KEY_SIZE +#define OPENPGP_VAULT_RECORD_SIZE PICOKEYS_VAULT_RECORD_SIZE +typedef uint8_t openpgp_vault_app_t; + +#define OPENPGP_VAULT_APP_FIDO 0u +#define OPENPGP_VAULT_APP_OPENPGP 1u +#define OPENPGP_VAULT_APP_PIV 2u +#define OPENPGP_VAULT_APP_COUNT 3u + +bool openpgp_vault_is_enrolled(void); +bool openpgp_vault_wrapper_available(openpgp_vault_app_t app); +int openpgp_vault_load_kvault(openpgp_vault_app_t app, uint8_t kvault[OPENPGP_VAULT_KEY_SIZE]); +int openpgp_vault_store_kvault(openpgp_vault_app_t app, const uint8_t kvault[OPENPGP_VAULT_KEY_SIZE]); +int openpgp_vault_clear_wrappers(void); +bool openpgp_vault_backup_authorized(openpgp_vault_app_t app); +int openpgp_vault_command(openpgp_vault_app_t app); + +#endif // _OPENPGP_VAULT_H_ diff --git a/tests/openpgp/openpgp_key_container_test.c b/tests/openpgp/openpgp_key_container_test.c index eec20c0..d991bd3 100644 --- a/tests/openpgp/openpgp_key_container_test.c +++ b/tests/openpgp/openpgp_key_container_test.c @@ -68,6 +68,11 @@ uint8_t session_rc[32]; uint8_t session_pw3[32]; uint8_t dek[IV_SIZE + 32]; +bool openpgp_vault_backup_authorized(openpgp_vault_app_t app) { + (void)app; + return false; +} + static test_file_t *test_file_from_handle(const file_t *file) { for (size_t i = 0; i < TEST_FILE_COUNT; i++) { if (&test_files[i].file == file) { diff --git a/tests/pico-openpgp/conftest.py b/tests/pico-openpgp/conftest.py new file mode 100644 index 0000000..c98977b --- /dev/null +++ b/tests/pico-openpgp/conftest.py @@ -0,0 +1,21 @@ +import sys +from pathlib import Path + +import pytest + + +OPENPGP_TEST_DIR = Path(__file__).parents[1] / "openpgp" +if str(OPENPGP_TEST_DIR) not in sys.path: + sys.path.insert(0, str(OPENPGP_TEST_DIR)) + + +@pytest.fixture(scope="session") +def card(): + from card_reader import get_ccid_device + from openpgp_card import OpenPGP_Card + + reader = get_ccid_device() + openpgp_card = OpenPGP_Card(reader) + openpgp_card.cmd_select_openpgp() + yield openpgp_card + reader.ccid_power_off() diff --git a/tests/pico-openpgp/test_080_vault.py b/tests/pico-openpgp/test_080_vault.py new file mode 100644 index 0000000..85c675a --- /dev/null +++ b/tests/pico-openpgp/test_080_vault.py @@ -0,0 +1,364 @@ +import hashlib +import os +import struct + +import pytest +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import x448 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + + +VAULT_MAGIC = b"PKV1" +VAULT_ID_DOMAIN = b"PicoKeys Vault ID v1" +VAULT_ENROLL_INFO = b"PicoKeys Vault enrollment v1" +VAULT_ID_BYTES = 32 +VAULT_ENROLL_CHALLENGE_BYTES = 32 +SERIAL_MAX = 16 +SERIAL_LENGTH_OFFSET = 68 +SERIAL_OFFSET = 69 +ALGORITHM_OFFSET = 85 +HEADER_LENGTH = 86 +NONCE_BYTES = 12 +TAG_BYTES = 16 +ALGORITHMS = (1, 2, 3, 4) + +INS_VAULT = 0xF2 +VAULT_STATUS = 0x01 +VAULT_START_ENROLLMENT = 0x02 +VAULT_FINISH_ENROLLMENT = 0x03 +VAULT_EXPORT = 0x04 +VAULT_IMPORT = 0x05 +VAULT_UNENROLL = 0x06 + +OPENPGP_AID = bytes.fromhex("D27600012401") +PIV_AID = bytes.fromhex("A000000308") + + +def _vault_id(kvault): + return hashlib.sha256(VAULT_ID_DOMAIN + kvault).digest() + + +def _object_hash(app, fid): + return hashlib.sha256(bytes([app, fid >> 8, fid & 0xFF])).digest() + + +def _layer_algorithm(algorithm, layer): + if algorithm == 3: + return (1, 2)[layer] + if algorithm == 4: + return (2, 1)[layer] + return algorithm + + +def _layer_key(kvault, vault_id, object_hash, algorithm, layer): + info = VAULT_ENROLL_INFO + object_hash + bytes([algorithm, layer]) + return HKDF(algorithm=hashes.SHA256(), length=32, salt=vault_id, info=info).derive(kvault) + + +def _aead(algorithm, key, nonce, data, aad, encrypt): + cipher = ChaCha20Poly1305(key) if algorithm == 1 else AESGCM(key) + if encrypt: + return cipher.encrypt(nonce, data, aad) + return cipher.decrypt(nonce, data, aad) + + +def _plain_object(app, fid): + private_data = b"private-key-data" + public_data = b"public-key-data" + return b"".join(( + b"\x01", + bytes([app]), + fid.to_bytes(2, "big"), + len(private_data).to_bytes(2, "big"), + private_data, + len(public_data).to_bytes(2, "big"), + public_data, + )) + + +def _export_blob(kvault, app, fid, algorithm, serial=b"0123456789ABCDEF", object_hash=None): + if algorithm not in ALGORITHMS or len(serial) > SERIAL_MAX: + raise ValueError("invalid vault blob parameters") + vault_id = _vault_id(kvault) + object_hash = _object_hash(app, fid) if object_hash is None else object_hash + header = VAULT_MAGIC + vault_id + object_hash + bytes([len(serial)]) + serial.ljust(SERIAL_MAX, b"\0") + bytes([algorithm]) + layers = 2 if algorithm >= 3 else 1 + nonces = b"".join(bytes([algorithm, layer]) + bytes(NONCE_BYTES - 2) for layer in range(layers)) + encrypted = _plain_object(app, fid) + for layer in range(layers): + encrypted = _aead(_layer_algorithm(algorithm, layer), _layer_key(kvault, vault_id, object_hash, algorithm, layer), nonces[layer * NONCE_BYTES:(layer + 1) * NONCE_BYTES], encrypted, header, True) + return header + nonces + encrypted + + +def _import_blob(kvault, blob): + if len(blob) < HEADER_LENGTH + NONCE_BYTES + TAG_BYTES or blob[:4] != VAULT_MAGIC: + raise ValueError("invalid vault blob") + algorithm = blob[ALGORITHM_OFFSET] + if algorithm not in ALGORITHMS or blob[SERIAL_LENGTH_OFFSET] > SERIAL_MAX: + raise ValueError("invalid vault blob") + layers = 2 if algorithm >= 3 else 1 + nonce_end = HEADER_LENGTH + layers * NONCE_BYTES + encrypted = blob[nonce_end:] + vault_id = blob[4:36] + object_hash = blob[36:68] + nonces = blob[HEADER_LENGTH:nonce_end] + for layer in range(layers - 1, -1, -1): + encrypted = _aead(_layer_algorithm(algorithm, layer), _layer_key(kvault, vault_id, object_hash, algorithm, layer), nonces[layer * NONCE_BYTES:(layer + 1) * NONCE_BYTES], encrypted, blob[:HEADER_LENGTH], False) + if len(encrypted) < 8 or encrypted[0] != 1: + raise ValueError("invalid vault object") + app = encrypted[1] + fid = int.from_bytes(encrypted[2:4], "big") + private_length = int.from_bytes(encrypted[4:6], "big") + public_length_offset = 6 + private_length + if public_length_offset + 2 > len(encrypted): + raise ValueError("invalid vault object") + public_length = int.from_bytes(encrypted[public_length_offset:public_length_offset + 2], "big") + if public_length_offset + 2 + public_length != len(encrypted): + raise ValueError("invalid vault object") + if _object_hash(app, fid) != object_hash: + raise ValueError("object hash mismatch") + return app, fid, encrypted[6:6 + private_length], encrypted[public_length_offset + 2:] + + +def _enrollment_packet(private_key, device_public, challenge, kvault, label): + certificate = b"certificate" + certificate_public = private_key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + info = VAULT_ENROLL_INFO + challenge + certificate_public + device_public + shared = private_key.exchange(x448.X448PublicKey.from_public_bytes(device_public)) + session_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=info).derive(shared) + label_bytes = label.encode() + if len(label_bytes) > 64: + raise ValueError("vault label is too long") + plain = kvault + bytes([len(label_bytes)]) + label_bytes + nonce = bytes(range(NONCE_BYTES)) + return struct.pack(">H", len(certificate)) + certificate + nonce + AESGCM(session_key).encrypt(nonce, plain, info) + + +def _reader(card): + return card._OpenPGP_Card__reader + + +def _compose_apdu(ins, p1, p2, data=b"", le=None): + data = bytes(data) + apdu = bytearray([0, ins, p1, p2]) + if data: + if len(data) < 256: + apdu.append(len(data)) + else: + apdu += bytes([0, len(data) >> 8, len(data) & 0xFF]) + apdu += data + if le is not None: + apdu += bytes([le]) if le < 256 else bytes([0, le >> 8, le & 0xFF]) + return bytes(apdu) + + +def _raw(card, ins, p1, p2, data=b"", le=None): + response = _reader(card).send_cmd(_compose_apdu(ins, p1, p2, data, le)) + body, status = response[:-2], response[-2:] + while status[0] == 0x61: + response = _reader(card).send_cmd(_compose_apdu(0xC0, 0, 0, le=status[1])) + body += response[:-2] + status = response[-2:] + return body, status + + +def _expect(card, ins, p1, p2, data=b"", status=b"\x90\x00"): + body, actual = _raw(card, ins, p1, p2, data, le=0 if not data else None) + assert actual == status + return body + + +def _select(card, aid): + return _expect(card, 0xA4, 0x04, 0, aid) + + +def _live_card(request): + if os.environ.get("PICO_OPENPGP_VAULT_LIVE") != "1": + pytest.skip("set PICO_OPENPGP_VAULT_LIVE=1 for live vault APDU tests") + return request.getfixturevalue("card") + + +def _live_admin_card(request): + card = _live_card(request) + password = os.environ.get("PICO_OPENPGP_VAULT_PW3") + if not password: + pytest.skip("set PICO_OPENPGP_VAULT_PW3 for authenticated live vault tests") + _select(card, OPENPGP_AID) + try: + card.verify(3, password.encode()) + except Exception as error: + pytest.skip(f"live PW3 unavailable: {error}") + return card + + +def test_vault_id_is_deterministic_and_256_bit(): + kvault = bytes(range(32)) + assert _vault_id(kvault) == _vault_id(kvault) + assert len(_vault_id(kvault)) == VAULT_ID_BYTES + assert _vault_id(kvault) != _vault_id(bytes(range(1, 33))) + + +@pytest.mark.parametrize("algorithm", ALGORITHMS) +def test_pk_v1_round_trip_for_every_algorithm(algorithm): + kvault = bytes(range(32)) + blob = _export_blob(kvault, 1, 0xB601, algorithm) + assert blob[:4] == VAULT_MAGIC + assert blob[ALGORITHM_OFFSET] == algorithm + assert blob[SERIAL_LENGTH_OFFSET] == SERIAL_MAX + assert _import_blob(kvault, blob) == (1, 0xB601, b"private-key-data", b"public-key-data") + + +def test_pk_v1_authenticates_header_and_ciphertext(): + blob = bytearray(_export_blob(bytes(range(32)), 1, 0xB601, 1)) + blob[SERIAL_OFFSET] ^= 1 + with pytest.raises(InvalidTag): + _import_blob(bytes(range(32)), bytes(blob)) + + blob = bytearray(_export_blob(bytes(range(32)), 1, 0xB601, 1)) + blob[-1] ^= 1 + with pytest.raises(InvalidTag): + _import_blob(bytes(range(32)), bytes(blob)) + + +def test_pk_v1_rejects_wrong_vault_key_and_object_identity(): + blob = _export_blob(bytes(range(32)), 1, 0xB601, 1) + with pytest.raises(InvalidTag): + _import_blob(bytes(range(1, 33)), blob) + + blob = _export_blob(bytes(range(32)), 1, 0xB601, 1, object_hash=_object_hash(1, 0xB602)) + with pytest.raises(ValueError, match="object hash mismatch"): + _import_blob(bytes(range(32)), blob) + + +@pytest.mark.parametrize("label", ["", "A" * 64]) +def test_enrollment_packet_accepts_label_boundaries(label): + private_key = x448.X448PrivateKey.generate() + device_private = x448.X448PrivateKey.generate() + packet = _enrollment_packet(private_key, device_private.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw), bytes(range(32)), bytes(range(32)), label) + certificate_length = int.from_bytes(packet[:2], "big") + assert packet[2 + certificate_length + 12:] + + +def test_enrollment_packet_rejects_oversized_label(): + with pytest.raises(ValueError): + _enrollment_packet(x448.X448PrivateKey.generate(), bytes(56), bytes(32), bytes(32), "A" * 65) + + +def test_enrollment_packet_tampering_fails_authentication(): + private_key = x448.X448PrivateKey.generate() + device_private = x448.X448PrivateKey.generate() + challenge = bytes(range(32)) + kvault = bytes(range(32)) + device_public = device_private.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + packet = bytearray(_enrollment_packet(private_key, device_public, challenge, kvault, "label")) + packet[-1] ^= 1 + certificate_length = int.from_bytes(packet[:2], "big") + certificate_public = private_key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + info = VAULT_ENROLL_INFO + challenge + certificate_public + device_public + shared = private_key.exchange(x448.X448PublicKey.from_public_bytes(device_public)) + session_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=info).derive(shared) + with pytest.raises(InvalidTag): + AESGCM(session_key).decrypt(bytes(packet[2 + certificate_length:2 + certificate_length + NONCE_BYTES]), bytes(packet[2 + certificate_length + NONCE_BYTES:]), info) + + +@pytest.mark.parametrize("blob", [b"", b"PKV", b"PKV2" + b"\0" * HEADER_LENGTH, VAULT_MAGIC + b"\0" * (HEADER_LENGTH - 1)]) +def test_pk_v1_rejects_malformed_blob(blob): + with pytest.raises(ValueError): + _import_blob(bytes(32), blob) + + +def test_pk_v1_serial_length_is_bounded(): + with pytest.raises(ValueError): + _export_blob(bytes(32), 1, 0xB601, 1, b"A" * (SERIAL_MAX + 1)) + + +def test_pk_v1_algorithm_is_bounded(): + with pytest.raises(ValueError): + _export_blob(bytes(32), 1, 0xB601, 0) + + +def test_live_openpgp_vault_status_contract(request): + card = _live_card(request) + _select(card, OPENPGP_AID) + status = _expect(card, INS_VAULT, VAULT_STATUS, 0) + assert len(status) >= 37 + assert status[0] == 1 + assert status[1] in (0, 1) + assert status[2] in (0, 1) + assert status[3] in (0, VAULT_ID_BYTES) + assert status[36] <= 64 + assert len(status) == 37 + status[36] + + +def test_live_vault_commands_require_openpgp_admin_pin(request): + card = _live_card(request) + _select(card, OPENPGP_AID) + card.deauthenticate(3) + _expect(card, INS_VAULT, VAULT_START_ENROLLMENT, 0, status=b"\x69\x82") + _expect(card, INS_VAULT, VAULT_EXPORT, 1, status=b"\x69\x82") + _expect(card, INS_VAULT, VAULT_UNENROLL, 0, status=b"\x69\x82") + + +def test_live_vault_dispatch_and_parameter_validation(request): + card = _live_card(request) + _select(card, OPENPGP_AID) + _expect(card, INS_VAULT, 0x07, 0, status=b"\x6A\x86") + _expect(card, INS_VAULT, VAULT_STATUS, 1, status=b"\x6A\x86") + _expect(card, INS_VAULT, VAULT_EXPORT, 0, status=b"\x6A\x86") + _expect(card, INS_VAULT, VAULT_IMPORT, 1, status=b"\x6A\x86") + + +def test_live_enrollment_begin_and_malformed_finish(request): + card = _live_admin_card(request) + response, status = _raw(card, INS_VAULT, VAULT_START_ENROLLMENT, 0) + if status != b"\x90\x00": + pytest.skip(f"enrollment start unavailable: {status.hex()}") + assert len(response) == 56 + VAULT_ENROLL_CHALLENGE_BYTES + _, status = _raw(card, INS_VAULT, VAULT_FINISH_ENROLLMENT, 0, b"\0") + assert status != b"\x90\x00" + + +def test_live_unknown_vault_subcommand_is_rejected(request): + card = _live_card(request) + _select(card, OPENPGP_AID) + _expect(card, INS_VAULT, 0x07, 0, status=b"\x6A\x86") + + +def test_live_export_import_roundtrip(request): + if os.environ.get("PICO_OPENPGP_VAULT_ROUNDTRIP") != "1": + pytest.skip("set PICO_OPENPGP_VAULT_ROUNDTRIP=1 for the live key round-trip") + card = _live_admin_card(request) + status = _expect(card, INS_VAULT, VAULT_STATUS, 0) + if status[3] != VAULT_ID_BYTES: + pytest.skip("OpenPGP vault is not enrolled") + blobs = [] + for algorithm in ALGORITHMS: + blob, result = _raw(card, INS_VAULT, VAULT_EXPORT, 1, bytes([algorithm])) + if result != b"\x90\x00": + pytest.skip(f"OpenPGP key handle 1 is not exportable: {result.hex()}") + assert blob[ALGORITHM_OFFSET] == algorithm + blobs.append(blob) + for blob in blobs: + _, result = _raw(card, INS_VAULT, VAULT_IMPORT, 1, blob) + assert result == b"\x90\x00" + + +def test_live_unenroll_requires_explicit_opt_in(request): + if os.environ.get("PICO_OPENPGP_VAULT_DESTRUCTIVE_TESTS") != "1": + pytest.skip("set PICO_OPENPGP_VAULT_DESTRUCTIVE_TESTS=1 to erase the device vault") + card = _live_admin_card(request) + _, status = _raw(card, INS_VAULT, VAULT_UNENROLL, 0) + assert status == b"\x90\x00" + response = _expect(card, INS_VAULT, VAULT_STATUS, 0) + assert response[3] == 0 + + +def test_live_piv_vault_dispatch(request): + card = _live_card(request) + _, status = _raw(card, 0xA4, 0x04, 0, PIV_AID) + if status != b"\x90\x00": + pytest.skip("connected card does not expose the PIV application") + response = _expect(card, INS_VAULT, VAULT_STATUS, 0) + assert len(response) >= 37