diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 3d72e974e..8203f1c22 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -7,6 +7,7 @@ paths: - lib - pkcs11 - src + - yhunwrap - yhwrap - ykhsmauth paths-ignore: diff --git a/.gitignore b/.gitignore index 9c97e124e..21c7c18f5 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ build*/ cscope.* yhauth/cmdline.c yhauth/cmdline.h +yhunwrap/cmdline.c +yhunwrap/cmdline.h yhwrap/cmdline.c yhwrap/cmdline.h yubihsm-auth/cmdline.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 11ffcfa37..22ae4fde8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -218,6 +218,8 @@ if(NOT BUILD_ONLY_LIB) add_subdirectory (examples) add_subdirectory(yhwrap) + + add_subdirectory(yhunwrap) endif() add_custom_target ( diff --git a/yhunwrap/CMakeLists.txt b/yhunwrap/CMakeLists.txt new file mode 100644 index 000000000..b5caf499b --- /dev/null +++ b/yhunwrap/CMakeLists.txt @@ -0,0 +1,88 @@ +# +# Copyright 2015-2018 Yubico AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +include(${CMAKE_SOURCE_DIR}/cmake/openssl.cmake) +find_libcrypto() + +set ( + SOURCE + ../common/hash.c + ../common/parsing.c + ../common/util.c + ../common/openssl-compat.c + main.c + ) + +if(WIN32) + set(SOURCE ${SOURCE} cmdline.c) + include(${CMAKE_SOURCE_DIR}/cmake/getopt.cmake) + find_getopt() +else(WIN32) + include(gengetopt) + add_gengetopt_files (cmdline) + set(SOURCE ${SOURCE} ${GGO_C}) + message("${GGO_C}") +endif(WIN32) + +include_directories ( + ${LIBCRYPTO_INCLUDEDIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../lib + ${CMAKE_CURRENT_SOURCE_DIR}/../common +) + +if(${WIN32}) + list(APPEND SOURCE ${CMAKE_CURRENT_BINARY_DIR}/version.rc) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/version.rc @ONLY) +endif(${WIN32}) + +# NOTE(adma): required by gengetopt +add_definitions (-DPACKAGE="yubihsm-unwrap") +add_definitions (-DVERSION="${yubihsm_shell_VERSION_MAJOR}.${yubihsm_shell_VERSION_MINOR}.${yubihsm_shell_VERSION_PATCH}") + +list(APPEND LCOV_REMOVE_PATTERNS "'${PROJECT_SOURCE_DIR}/yhunwrap/cmdline.c'") + +if ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") +set_property(SOURCE ${GGO_C} APPEND_STRING PROPERTY COMPILE_FLAGS " -Wno-unused-but-set-variable ") +endif() + +add_executable (yubihsm-unwrap ${SOURCE}) + +target_link_libraries ( + yubihsm-unwrap + ${LIBCRYPTO_LDFLAGS} + ${GETOPT_LIBS} + yubihsm + ) + +set_target_properties(yubihsm-unwrap PROPERTIES INSTALL_RPATH "${YUBIHSM_INSTALL_LIB_DIR}") + +add_coverage(yubihsm-unwrap) + +install( + TARGETS yubihsm-unwrap + ARCHIVE DESTINATION "${YUBIHSM_INSTALL_LIB_DIR}" + LIBRARY DESTINATION "${YUBIHSM_INSTALL_LIB_DIR}" + RUNTIME DESTINATION "${YUBIHSM_INSTALL_BIN_DIR}") + +if (NOT WITHOUT_MANPAGES) + include (help2man) + add_help2man_manpage (yubihsm-unwrap.1 yubihsm-unwrap) + + add_custom_target (yubihsm-unwrap-man ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/yubihsm-unwrap.1 + ) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/yubihsm-unwrap.1" DESTINATION "${YUBIHSM_INSTALL_MAN_DIR}/man1") +endif () diff --git a/yhunwrap/README.adoc b/yhunwrap/README.adoc new file mode 100644 index 000000000..cdf3f9683 --- /dev/null +++ b/yhunwrap/README.adoc @@ -0,0 +1,41 @@ +== YubiHSM Unwrap + +YubiHSM Unwrap is a command-line tool to decrypt "offline wraps" +from a YubiHSM 2 device. See `yubihsm-wrap` to create "offline wraps" +or key backups encrypted with a wrap key. + +One of the functionalities supported by the YubiHSM is to import +objects under wrap. The typical use is to generate an object on one +device, export it under wrap using a Wrap Key and import it to a +different device which has the same Wrap Key. + +At times it is also useful to be able to decrypt these keys under wrap +on a computer, so that they can be encrypted using alternative methods and +also easily sent to other types of devices for use. + +=== Example + +This example assumes that you created a wrapped object by exporting +a key from a YubiHSM device or using the example in the `yubihsm-wrap` +documentation. Make sure the wrap key is the binary format documented +under `yubihsm-wrap`. + +For example the wrap key +`00112233445566778899aabbccddeeff` + +can be saved as `wrap.key` by running + +[source, bash] +---- +$ echo -en '\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff' >wrap.key +---- + +We can now use `yubihsm-unwrap` to produce the decrypted version of the +private key. + +[source, bash] +---- +$ yubihsm-unwrap --in private.yhw --wrapkey wrap.key --out private.pem +---- + +The output file `private.pem` is the unwrapped version of the key. diff --git a/yhunwrap/cmdline.ggo b/yhunwrap/cmdline.ggo new file mode 100644 index 000000000..b38b4c5a0 --- /dev/null +++ b/yhunwrap/cmdline.ggo @@ -0,0 +1,19 @@ +# +# Copyright 2015-2018 Yubico AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +option "in" - "Input data (filename)" string +option "out" - "Output data (filename)" string +option "wrapkey" k "Key to wrap data with (filename)" string diff --git a/yhunwrap/main.c b/yhunwrap/main.c new file mode 100644 index 000000000..57a7b8fa4 --- /dev/null +++ b/yhunwrap/main.c @@ -0,0 +1,350 @@ +/* + * Copyright 2015-2018 Yubico AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include "../common/platform-config.h" + +#ifdef __WIN32 +#include +#else +#include +#endif + +#include +#include + +#include "cmdline.h" + +#include "parsing.h" +#include "util.h" + +#include + +#define INPUT_BUFSIZE 4096 +#define WRAPKEY_BUFSIZE 32 + +#define OBJECT_HEADER_SIZE 59 + +static bool unwrap_data(uint8_t *key, size_t key_len, uint8_t *in, size_t in_len, + uint8_t *out, size_t *out_len) { + + EVP_CIPHER_CTX *ctx = NULL; + const EVP_CIPHER *cipher_type; + + uint8_t nonce[13]; + int nonce_len = 13; + int tag_len = 16; + + int len; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + return false; + } + + switch (key_len) { + case 16: + cipher_type = EVP_aes_128_ccm(); + break; + + case 24: + cipher_type = EVP_aes_192_ccm(); + break; + + case 32: + cipher_type = EVP_aes_256_ccm(); + break; + + default: + return false; + } + + memcpy(nonce, in, nonce_len); + + // Select cipher + if (EVP_DecryptInit_ex(ctx, cipher_type, NULL, NULL, NULL) != 1) { + return false; + } + + // Set nonce length + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, nonce_len, NULL) != 1) { + return false; + } + + // Set tag + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, tag_len, in + in_len - tag_len) != 1) { + return false; + } + + // Initialize key and IV + if (EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce) != 1) { + return false; + } + + // Provide the total ciphertext length + if (EVP_DecryptUpdate(ctx, NULL, &len, NULL, in_len - nonce_len - tag_len) != 1) { + return false; + } + + // Provide the message to be decrypted, and obtain the decrypted output + if (EVP_DecryptUpdate(ctx, out, &len, in + nonce_len, in_len - nonce_len - tag_len) != 1) { + return false; + } + *out_len = len; + + // Finalize decryption for completeness. (AES-CCM gets no output from DecryptFinal.) + if (EVP_DecryptFinal(ctx, out + *out_len, &len) != 1) { + return false; + } + *out_len += len; + + // Clean up + EVP_CIPHER_CTX_free(ctx); + + return true; +} + +static FILE *open_file(const char *name, bool input) { + if (input) { + if (strcmp(name, "-") == 0) { + return stdin; + } else { + return fopen(name, "rb"); + } + } else { + if (strcmp(name, "-") == 0) { + return stdout; + } else { + return fopen(name, "wb"); + } + } +} + +static void dump_hex(const unsigned char *buf, unsigned int len) { + unsigned int i; + for (i = 0; i < len; i++) { + if (i && !(i % 32)) + fprintf(stderr, "\n"); + else if (i && !(i % 8)) + fprintf(stderr, " "); + fprintf(stderr, "%02x", buf[i]); + } +} + +static void print_header(uint8_t *header) { + + uint8_t header_index = 0; + fprintf(stderr, "Wrapkey algorithm: %02x, ", header[header_index]); + switch (header[header_index]) { + case YH_ALGO_AES128_CCM_WRAP: + fprintf(stderr, "AES128-CCM\n"); + break; + case YH_ALGO_AES192_CCM_WRAP: + fprintf(stderr, "AES192-CCM\n"); + break; + case YH_ALGO_AES256_CCM_WRAP: + fprintf(stderr, "AES256-CCM\n"); + break; + default: + fprintf(stderr, "UNKNOWN\n"); + } + header_index+=sizeof(uint8_t); + + yh_capabilities capabilities; + memcpy(capabilities.capabilities, header + header_index, YH_CAPABILITIES_LEN); + + const char *cap[sizeof(yh_capability) / sizeof(yh_capability[0])] = {0}; + size_t n_cap = sizeof(yh_capability) / sizeof(yh_capability[0]); + + fprintf(stderr, "Capabilities: "); + dump_hex(capabilities.capabilities, YH_CAPABILITIES_LEN); + fprintf(stderr, ", "); + if (yh_capabilities_to_strings(&capabilities, cap, &n_cap) != + YHR_SUCCESS) { + for (size_t i = 0; i < YH_CAPABILITIES_LEN; i++) { + fprintf(stderr, "0x%02x%s", capabilities.capabilities[i], + i < YH_CAPABILITIES_LEN - 1 ? " " : ""); + } + } else { + for (size_t i = 0; i < n_cap; i++) { + fprintf(stderr, "%s%s", cap[i], i < n_cap - 1 ? ":" : ""); + } + } + fprintf(stderr, "\n"); + header_index+=YH_CAPABILITIES_LEN; + + uint16_t id; + memcpy(&id, header + header_index, sizeof(uint16_t)); + fprintf(stderr, "ID: %04x\n", ntohs(id)); + header_index+=sizeof(uint16_t); + + uint16_t data_len; + memcpy(&data_len, header + header_index, sizeof(uint16_t)); + fprintf(stderr, "Key size: %04x\n", ntohs(data_len)); + header_index+=sizeof(uint16_t); + + uint16_t object_domains; + char domains[256] = {0}; + memcpy(&object_domains, header + header_index, sizeof(uint16_t)); + yh_domains_to_string(ntohs(object_domains), domains, 255); + fprintf(stderr, "Domains: %04x, %s\n", ntohs(object_domains), domains); + header_index+=sizeof(uint16_t); + + uint8_t object_type; + const char *type = 0; + memcpy(&object_type, header + header_index, sizeof(uint8_t)); + yh_type_to_string(object_type, &type); + fprintf(stderr, "Type: %02x, %s\n", object_type, type); + header_index+=sizeof(uint8_t); + + uint8_t object_algorithm; + const char *algorithm = ""; + memcpy(&object_algorithm, header + header_index, sizeof(uint8_t)); + yh_algo_to_string(object_algorithm, &algorithm); + fprintf(stderr, "Algorithm: %02x, %s\n", object_algorithm, algorithm); + header_index+=sizeof(uint8_t); + + uint8_t sequence; + memcpy(&sequence, header + header_index, sizeof(uint8_t)); + fprintf(stderr, "Sequence: %02x\n", sequence); + header_index+=sizeof(uint8_t); + + uint8_t object_origin; + memcpy(&object_origin, header + header_index, sizeof(uint8_t)); + fprintf(stderr, "Origin: %02x, ", object_origin); + if (object_origin & YH_ORIGIN_GENERATED) { + fprintf(stderr, "generated"); + } + if (object_origin & YH_ORIGIN_IMPORTED) { + fprintf(stderr, "imported"); + } + if (object_origin & YH_ORIGIN_IMPORTED_WRAPPED) { + fprintf(stderr, ":imported_wrapped"); + } + fprintf(stderr, "\n"); + header_index+=sizeof(uint8_t); + + uint8_t label[YH_OBJ_LABEL_LEN] = {0}; + memcpy(&label, header + header_index, YH_OBJ_LABEL_LEN); + fprintf(stderr, "Label: %.*s\n", YH_OBJ_LABEL_LEN, label); + header_index+=sizeof(YH_OBJ_LABEL_LEN); +} + +int main(int argc, char *argv[]) { + struct gengetopt_args_info args_info; + + int rc = EXIT_FAILURE; + + FILE *input_file = NULL; + FILE *output_file = NULL; + FILE *wrapkey_file = NULL; + + uint8_t wrapped[2048] = {0}; + size_t wrapped_len = sizeof(wrapped); + + if (cmdline_parser(argc, argv, &args_info) != 0) { + goto main_exit; + } + + input_file = open_file(args_info.in_arg, true); + if (input_file == NULL) { + perror("Unable to open input file"); + goto main_exit; + } + + if (read_file(input_file, wrapped, &wrapped_len) == false) { + fprintf(stderr, "Unable to read input file\n"); + goto main_exit; + } + + // Optionally, base64-decode the input key. + base64_decode((char *)wrapped, wrapped, &wrapped_len); + + wrapkey_file = open_file(args_info.wrapkey_arg, true); + if (wrapkey_file == NULL) { + perror("Unable to open wrapkey file"); + goto main_exit; + } + + uint8_t wrapkey_buf[WRAPKEY_BUFSIZE]; + size_t wrapkey_buf_len = sizeof(wrapkey_buf); + if (read_file(wrapkey_file, wrapkey_buf, &wrapkey_buf_len) == false) { + fprintf(stderr, "Unable to read wrapkey file\n"); + } + + output_file = open_file(args_info.out_arg, false); + if (output_file == NULL) { + perror("Unable to open output file"); + goto main_exit; + } + +#pragma pack(push, 1) + union { + struct { + uint8_t header[OBJECT_HEADER_SIZE]; + uint8_t body[INPUT_BUFSIZE]; + }; + uint8_t buf[1]; + } wrap_object = {{{0}, {0}}}; +#pragma pack(pop) + size_t wrap_object_len = sizeof(wrap_object.buf); + + if (unwrap_data(wrapkey_buf, wrapkey_buf_len, wrapped, + wrapped_len, wrap_object.buf, + &wrap_object_len) == false) { + fprintf(stderr, "Unable to unwrap data\n"); + goto main_exit; + } + + if (getenv("DEBUG") != NULL) { + print_header(wrap_object.header); + fprintf(stderr, "\n"); + } + if (write_file(wrap_object.body, wrap_object_len - OBJECT_HEADER_SIZE, output_file, _base64) == false || + write_file((uint8_t *) "\n", 1, output_file, _binary) == false) { + fprintf(stderr, "Unable to write output file\n"); + goto main_exit; + } + + rc = EXIT_SUCCESS; + +main_exit: + + cmdline_parser_free(&args_info); + + if (input_file != NULL) { + fclose(input_file); + input_file = NULL; + } + + if (output_file != NULL) { + fclose(output_file); + output_file = NULL; + } + + if (wrapkey_file != NULL) { + fclose(wrapkey_file); + wrapkey_file = NULL; + } + + return rc; +} diff --git a/yhunwrap/version.rc.in b/yhunwrap/version.rc.in new file mode 100644 index 000000000..d6187d5fe --- /dev/null +++ b/yhunwrap/version.rc.in @@ -0,0 +1,38 @@ + +#include + +#define VER_FILEVERSION @yubihsm_shell_VERSION_MAJOR@,@yubihsm_shell_VERSION_MINOR@,@yubihsm_shell_VERSION_PATCH@,0 +#define VER_FILEVERSION_STR "@yubihsm_shell_VERSION_MAJOR@.@yubihsm_shell_VERSION_MINOR@.@yubihsm_shell_VERSION_PATCH@.0" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VER_FILEVERSION + PRODUCTVERSION VER_FILEVERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Yubico AB" + VALUE "FileDescription", "YubiHSM Unwrap" + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "InternalName", "yubihsm-unwrap.exe" + VALUE "LegalCopyright", "\xa9 Yubico AB" + VALUE "OriginalFilename", "yubihsm-unwrap.exe" + VALUE "ProductName", "YubiHSM" + VALUE "ProductVersion", VER_FILEVERSION_STR + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END