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