mirror of
https://github.com/polhenarejos/pico-openpgp.git
synced 2026-08-26 22:48:47 +01:00
Add Pico Vault to OpenPGP and PIV
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
Submodule pico-keys-sdk updated: 9eedb0ef8d...c791c1be12
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user