Add Pico Vault to OpenPGP and PIV

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos
2026-08-25 14:31:17 +02:00
parent 5945f39781
commit 3f437db1c2
16 changed files with 1092 additions and 11 deletions
+1
View File
@@ -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
+5
View File
@@ -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();
+5 -3
View File
@@ -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
};
+1
View File
@@ -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
+6
View File
@@ -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;
}
+34
View File
@@ -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;
+4
View File
@@ -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);
+2 -2
View File
@@ -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 }
};
+5
View File
@@ -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))
+557
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
#include "vault.h"
#include <string.h>
#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);
}
+36 -5
View File
@@ -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 }
};
+45
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
#ifndef _OPENPGP_VAULT_H_
#define _OPENPGP_VAULT_H_
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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_
@@ -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) {
+21
View File
@@ -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()
+364
View File
@@ -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