mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-08-28 09:34:11 +00:00
948 lines
35 KiB
C
948 lines
35 KiB
C
//-----------------------------------------------------------------------------
|
|
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU 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 General Public License for more details.
|
|
//
|
|
// See LICENSE.txt for the text of the license.
|
|
//-----------------------------------------------------------------------------
|
|
// MIFARE DUOX certificate crypto helpers
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include "crypto/duoxcrypto.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <mbedtls/asn1.h>
|
|
#include <mbedtls/ecdsa.h>
|
|
#include <mbedtls/ecp.h>
|
|
#include <mbedtls/md.h>
|
|
#include <mbedtls/pk.h>
|
|
|
|
#include "common.h"
|
|
#include "commonutil.h"
|
|
#include "crypto/asn1utils.h"
|
|
#include "crypto/libpcrypto.h"
|
|
#include "fileutils.h"
|
|
#include "ui.h"
|
|
#include "util.h"
|
|
#include "x509_crt.h"
|
|
|
|
#define DUOX_CERTIFICATE_ANCHOR_INPUT_LEN 8192
|
|
#define DUOX_CERTIFICATE_ANCHOR_SEARCH_DEPTH 4
|
|
|
|
static int duox_cert_info_from_x509_crt(const mbedtls_x509_crt *cert, duox_cert_info_t *out);
|
|
|
|
const char *duox_certificate_format_name(duox_certificate_format_t format) {
|
|
switch (format) {
|
|
case DUOX_CERTIFICATE_FORMAT_X509:
|
|
return "X.509";
|
|
case DUOX_CERTIFICATE_FORMAT_GP_VDE:
|
|
return "GP VDE";
|
|
case DUOX_CERTIFICATE_FORMAT_UNKNOWN:
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
int duox_certificate_anchor_public_key(const duox_certificate_anchor_t *anchor,
|
|
mbedtls_ecp_group_id *curveid,
|
|
const uint8_t **pubkey, size_t *pubkey_len) {
|
|
if (anchor == NULL || curveid == NULL || pubkey == NULL || pubkey_len == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
switch (anchor->type) {
|
|
case DUOX_CERTIFICATE_ANCHOR_MATERIAL_NONE:
|
|
return PM3_EINVARG;
|
|
case DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY:
|
|
*curveid = anchor->material.key.curveid;
|
|
*pubkey = anchor->material.key.pubkey;
|
|
*pubkey_len = anchor->material.key.pubkey_len;
|
|
break;
|
|
case DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT:
|
|
*curveid = anchor->material.cert.curveid;
|
|
*pubkey = anchor->material.cert.pubkey;
|
|
*pubkey_len = anchor->material.cert.pubkey_len;
|
|
break;
|
|
default:
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
return (*curveid != MBEDTLS_ECP_DP_NONE && *pubkey != NULL && *pubkey_len > 0) ? PM3_SUCCESS : PM3_EINVARG;
|
|
}
|
|
|
|
const char *duox_certificate_anchor_subject(const duox_certificate_anchor_t *anchor) {
|
|
if (anchor == NULL || anchor->type != DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT) {
|
|
return "";
|
|
}
|
|
return anchor->material.cert.subject;
|
|
}
|
|
|
|
const char *duox_certificate_anchor_display_name(const duox_certificate_anchor_t *anchor) {
|
|
if (anchor == NULL) {
|
|
return "unknown certificate anchor";
|
|
}
|
|
if (anchor->name[0] != '\0') {
|
|
return anchor->name;
|
|
}
|
|
if (anchor->source[0] != '\0') {
|
|
return anchor->source;
|
|
}
|
|
if (anchor->type == DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY) {
|
|
return "provided public key";
|
|
}
|
|
return "provided certificate";
|
|
}
|
|
|
|
static int duox_load_x509_certificate_input(const char *input, mbedtls_x509_crt *cert) {
|
|
if (input == NULL || cert == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char normalized[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0};
|
|
size_t input_len = strlen(input);
|
|
if (input_len >= sizeof(normalized)) {
|
|
return PM3_EOVFLOW;
|
|
}
|
|
memcpy(normalized, input, input_len + 1);
|
|
str_trim_ascii_inplace(normalized);
|
|
if (normalized[0] == '\0') {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char *resolved_path = NULL;
|
|
if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) {
|
|
int ret = mbedtls_x509_crt_parse_file(cert, resolved_path);
|
|
free(resolved_path);
|
|
if (ret == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
}
|
|
|
|
if (mbedtls_x509_crt_parse(cert, (const unsigned char *)normalized, strlen(normalized) + 1) == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
char compact[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0};
|
|
size_t compact_len = 0;
|
|
if (str_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) != PM3_SUCCESS || compact_len == 0) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
size_t der_capacity = (compact_len / 2) + 1;
|
|
uint8_t *der = calloc(der_capacity, sizeof(uint8_t));
|
|
if (der == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
int der_len = hex_to_bytes(compact, der, der_capacity);
|
|
int res = (der_len > 0 && mbedtls_x509_crt_parse_der(cert, der, (size_t)der_len) == 0) ? PM3_SUCCESS : PM3_EINVARG;
|
|
free(der);
|
|
return res;
|
|
}
|
|
|
|
static int duox_load_certificate_anchor_from_certificate_input(const char *input, const char *name_hint,
|
|
const char *source, duox_certificate_anchor_t *anchor) {
|
|
if (input == NULL || anchor == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
duox_certificate_anchor_t candidate = {0};
|
|
mbedtls_x509_crt cert;
|
|
mbedtls_x509_crt_init(&cert);
|
|
int res = duox_load_x509_certificate_input(input, &cert);
|
|
if (res == PM3_SUCCESS) {
|
|
res = duox_cert_info_from_x509_crt(&cert, &candidate.material.cert);
|
|
}
|
|
mbedtls_x509_crt_free(&cert);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
|
|
candidate.type = DUOX_CERTIFICATE_ANCHOR_MATERIAL_CERT;
|
|
if (source != NULL && source[0] != '\0') {
|
|
str_copy(candidate.source, sizeof(candidate.source), source);
|
|
}
|
|
if (name_hint != NULL && name_hint[0] != '\0') {
|
|
str_copy(candidate.name, sizeof(candidate.name), name_hint);
|
|
} else if (candidate.material.cert.subject[0] != '\0') {
|
|
str_copy(candidate.name, sizeof(candidate.name), candidate.material.cert.subject);
|
|
} else {
|
|
str_copy(candidate.name, sizeof(candidate.name), "x509 certificate anchor");
|
|
}
|
|
|
|
*anchor = candidate;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int duox_load_certificate_anchor_from_pubkey_input(const char *input, const char *name_hint,
|
|
const char *source, duox_certificate_anchor_t *anchor) {
|
|
if (input == NULL || anchor == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
static const mbedtls_ecp_group_id curves[] = {
|
|
MBEDTLS_ECP_DP_SECP256R1,
|
|
MBEDTLS_ECP_DP_SECP384R1,
|
|
MBEDTLS_ECP_DP_SECP521R1,
|
|
MBEDTLS_ECP_DP_SECP224R1,
|
|
MBEDTLS_ECP_DP_SECP192R1,
|
|
MBEDTLS_ECP_DP_BP256R1,
|
|
MBEDTLS_ECP_DP_SECP256K1,
|
|
};
|
|
|
|
for (size_t i = 0; i < ARRAYLEN(curves); i++) {
|
|
const mbedtls_ecp_curve_info *curve_info = mbedtls_ecp_curve_info_from_grp_id(curves[i]);
|
|
if (curve_info == NULL) {
|
|
continue;
|
|
}
|
|
|
|
duox_certificate_anchor_t candidate = {0};
|
|
candidate.type = DUOX_CERTIFICATE_ANCHOR_MATERIAL_PUBLIC_KEY;
|
|
if (source != NULL && source[0] != '\0') {
|
|
str_copy(candidate.source, sizeof(candidate.source), source);
|
|
}
|
|
if (name_hint != NULL && name_hint[0] != '\0') {
|
|
str_copy(candidate.name, sizeof(candidate.name), name_hint);
|
|
} else {
|
|
str_copy(candidate.name, sizeof(candidate.name), "provided public key");
|
|
}
|
|
|
|
size_t coord_len = (curve_info->bit_size + 7) / 8;
|
|
size_t pubkey_len = 1 + (2 * coord_len);
|
|
if (pubkey_len > sizeof(candidate.material.key.pubkey)) {
|
|
continue;
|
|
}
|
|
|
|
if (ensure_ec_public_key(input, curves[i], candidate.material.key.pubkey, pubkey_len) == PM3_SUCCESS) {
|
|
candidate.material.key.curveid = curves[i];
|
|
candidate.material.key.pubkey_len = pubkey_len;
|
|
*anchor = candidate;
|
|
return PM3_SUCCESS;
|
|
}
|
|
}
|
|
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
static int duox_load_certificate_anchor_from_file_path(const char *filepath, duox_certificate_anchor_t *anchor) {
|
|
if (filepath == NULL || anchor == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0};
|
|
path_basename_without_ext(filepath, filename_anchor_name, sizeof(filename_anchor_name));
|
|
if (filename_anchor_name[0] == '\0') {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
int res = duox_load_certificate_anchor_from_certificate_input(filepath, filename_anchor_name, filepath, anchor);
|
|
if (res == PM3_SUCCESS) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
return duox_load_certificate_anchor_from_pubkey_input(filepath, filename_anchor_name, filepath, anchor);
|
|
}
|
|
|
|
static int duox_load_named_certificate_anchor_from_store(const char *token, const char *anchor_store_dir,
|
|
duox_certificate_anchor_t *anchor) {
|
|
if (token == NULL || anchor_store_dir == NULL || anchor == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char paths[DUOX_CERTIFICATE_ANCHOR_MAX_PATHS][DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {{0}};
|
|
size_t path_count = 0;
|
|
int res = collect_resource_file_paths(anchor_store_dir, (char *)paths, sizeof(paths[0]), ARRAYLEN(paths), &path_count,
|
|
false, DUOX_CERTIFICATE_ANCHOR_SEARCH_DEPTH);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
|
|
for (size_t i = 0; i < path_count; i++) {
|
|
char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0};
|
|
path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name));
|
|
if (filename_anchor_name[0] == '\0') {
|
|
continue;
|
|
}
|
|
if (!str_equal_case_insensitive(token, filename_anchor_name)) {
|
|
continue;
|
|
}
|
|
return duox_load_certificate_anchor_from_file_path(paths[i], anchor);
|
|
}
|
|
|
|
const char *matched_path = NULL;
|
|
for (size_t i = 0; i < path_count; i++) {
|
|
char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0};
|
|
path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name));
|
|
if (filename_anchor_name[0] == '\0' || !str_startswith_case_insensitive(filename_anchor_name, token)) {
|
|
continue;
|
|
}
|
|
if (matched_path != NULL) {
|
|
return PM3_EOVFLOW;
|
|
}
|
|
matched_path = paths[i];
|
|
}
|
|
|
|
if (matched_path != NULL) {
|
|
return duox_load_certificate_anchor_from_file_path(matched_path, anchor);
|
|
}
|
|
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
int duox_load_certificate_anchor_from_input(const char *input, const char *anchor_store_dir,
|
|
duox_certificate_anchor_t *anchor) {
|
|
if (input == NULL || anchor == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char normalized[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0};
|
|
snprintf(normalized, sizeof(normalized), "%s", input);
|
|
str_trim_ascii_inplace(normalized);
|
|
if (normalized[0] == '\0') {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char *resolved_path = NULL;
|
|
if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) {
|
|
int res = PM3_EINVARG;
|
|
if (path_is_regular_file(resolved_path)) {
|
|
res = duox_load_certificate_anchor_from_file_path(resolved_path, anchor);
|
|
}
|
|
free(resolved_path);
|
|
if (res == PM3_SUCCESS) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
}
|
|
|
|
if (anchor_store_dir != NULL && anchor_store_dir[0] != '\0') {
|
|
int res = duox_load_named_certificate_anchor_from_store(normalized, anchor_store_dir, anchor);
|
|
if (res == PM3_SUCCESS) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
}
|
|
|
|
int res = duox_load_certificate_anchor_from_certificate_input(normalized, NULL, "inline certificate", anchor);
|
|
if (res == PM3_SUCCESS) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
return duox_load_certificate_anchor_from_pubkey_input(normalized, NULL, "inline public key", anchor);
|
|
}
|
|
|
|
int duox_load_certificate_anchors_from_store(const char *anchor_store_dir,
|
|
duox_certificate_anchor_t *anchors,
|
|
size_t max_anchors, size_t *out_count) {
|
|
if (anchor_store_dir == NULL || anchors == NULL || out_count == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char paths[DUOX_CERTIFICATE_ANCHOR_MAX_PATHS][DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {{0}};
|
|
size_t path_count = 0;
|
|
int res = collect_resource_file_paths(anchor_store_dir, (char *)paths, sizeof(paths[0]), ARRAYLEN(paths), &path_count,
|
|
false, DUOX_CERTIFICATE_ANCHOR_SEARCH_DEPTH);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
if (path_count == 0) {
|
|
return PM3_EFILE;
|
|
}
|
|
|
|
size_t count = 0;
|
|
for (size_t i = 0; i < path_count && count < max_anchors; i++) {
|
|
res = duox_load_certificate_anchor_from_file_path(paths[i], &anchors[count]);
|
|
if (res == PM3_SUCCESS) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count == 0) {
|
|
return PM3_EFILE;
|
|
}
|
|
|
|
*out_count = count;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int duox_asn1_seq_full_length(const uint8_t *data, size_t datalen, size_t *fulllen) {
|
|
if (data == NULL || fulllen == NULL || datalen < 2) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
unsigned char *p = (unsigned char *)data;
|
|
const unsigned char *end = data + datalen;
|
|
size_t body_len = 0;
|
|
int res = mbedtls_asn1_get_tag(&p, end, &body_len, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE);
|
|
if (res != 0 || body_len > (size_t)(end - p)) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
*fulllen = (size_t)(p - data) + body_len;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int duox_parse_x509_der_crt(const uint8_t *data, size_t data_len, bool verbose, mbedtls_x509_crt *cert) {
|
|
if (data == NULL || data_len == 0 || cert == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
size_t detected_len = 0;
|
|
if (duox_asn1_seq_full_length(data, data_len, &detected_len) != PM3_SUCCESS || detected_len != data_len) {
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
int xres = mbedtls_x509_crt_parse_der(cert, data, data_len);
|
|
if (xres == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
if (verbose) {
|
|
PrintAndLogEx(INFO, "X.509 parser rejected certificate data (0x%x)",
|
|
(xres < 0) ? -xres : xres);
|
|
}
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
static int duox_x509_public_key_from_cert(const mbedtls_x509_crt *cert,
|
|
mbedtls_ecp_group_id *curveid,
|
|
uint8_t *out_pub, size_t out_pub_capacity, size_t *out_pub_len) {
|
|
if (cert == NULL || out_pub == NULL || out_pub_len == NULL || out_pub_capacity == 0) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
if (!mbedtls_pk_can_do(&cert->pk, MBEDTLS_PK_ECKEY) &&
|
|
!mbedtls_pk_can_do(&cert->pk, MBEDTLS_PK_ECDSA)) {
|
|
return PM3_ENOTIMPL;
|
|
}
|
|
|
|
mbedtls_ecp_keypair *ec = mbedtls_pk_ec(cert->pk);
|
|
if (ec == NULL) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
size_t coord_len = (ec->grp.nbits + 7) / 8;
|
|
size_t expected_len = 1 + (2 * coord_len);
|
|
if (expected_len > out_pub_capacity) {
|
|
return PM3_EOVFLOW;
|
|
}
|
|
|
|
size_t written = 0;
|
|
if (mbedtls_ecp_point_write_binary(&ec->grp, &ec->Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &written, out_pub, out_pub_capacity) != 0 ||
|
|
written != expected_len) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
if (curveid != NULL) {
|
|
*curveid = ec->grp.id;
|
|
}
|
|
*out_pub_len = written;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int duox_cert_info_from_x509_crt(const mbedtls_x509_crt *cert, duox_cert_info_t *out) {
|
|
if (cert == NULL || out == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
duox_cert_info_t info = {0};
|
|
info.format = DUOX_CERTIFICATE_FORMAT_X509;
|
|
mbedtls_x509_dn_gets(info.issuer, sizeof(info.issuer), &cert->issuer);
|
|
mbedtls_x509_dn_gets(info.subject, sizeof(info.subject), &cert->subject);
|
|
snprintf(info.serial, sizeof(info.serial), "%s", sprint_hex_inrow(cert->serial.p, cert->serial.len));
|
|
snprintf(info.valid_from, sizeof(info.valid_from), "%04d-%02d-%02d %02d:%02d:%02d",
|
|
cert->valid_from.year, cert->valid_from.mon, cert->valid_from.day,
|
|
cert->valid_from.hour, cert->valid_from.min, cert->valid_from.sec);
|
|
snprintf(info.valid_to, sizeof(info.valid_to), "%04d-%02d-%02d %02d:%02d:%02d",
|
|
cert->valid_to.year, cert->valid_to.mon, cert->valid_to.day,
|
|
cert->valid_to.hour, cert->valid_to.min, cert->valid_to.sec);
|
|
|
|
int res = duox_x509_public_key_from_cert(cert, &info.curveid, info.pubkey, sizeof(info.pubkey), &info.pubkey_len);
|
|
*out = info;
|
|
return res;
|
|
}
|
|
|
|
int duox_parse_x509_certificate(const uint8_t *data, size_t data_len,
|
|
bool verbose, duox_cert_info_t *out) {
|
|
if (data == NULL || data_len == 0 || out == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
mbedtls_x509_crt cert;
|
|
mbedtls_x509_crt_init(&cert);
|
|
int res = duox_parse_x509_der_crt(data, data_len, verbose, &cert);
|
|
if (res == PM3_SUCCESS) {
|
|
res = (duox_cert_info_from_x509_crt(&cert, out) == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ECRYPTO;
|
|
}
|
|
mbedtls_x509_crt_free(&cert);
|
|
return res;
|
|
}
|
|
|
|
static int duox_verify_x509_signature_with_public_key(const mbedtls_x509_crt *cert,
|
|
mbedtls_ecp_group_id signer_curveid,
|
|
const uint8_t *signer_pubkey, size_t signer_pubkey_len,
|
|
bool verbose) {
|
|
if (cert == NULL || signer_pubkey == NULL || signer_pubkey_len == 0 ||
|
|
signer_curveid == MBEDTLS_ECP_DP_NONE) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (cert->sig_md == MBEDTLS_MD_NONE || mbedtls_md_info_from_type(cert->sig_md) == NULL) {
|
|
if (verbose) {
|
|
PrintAndLogEx(WARNING, "Unsupported X.509 signature hash");
|
|
}
|
|
return PM3_ENOTIMPL;
|
|
}
|
|
if (cert->sig_pk != MBEDTLS_PK_ECDSA) {
|
|
if (verbose) {
|
|
PrintAndLogEx(WARNING, "Unsupported X.509 signature public key type (need ECDSA)");
|
|
}
|
|
return PM3_ENOTIMPL;
|
|
}
|
|
|
|
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(cert->sig_md);
|
|
uint8_t digest[MBEDTLS_MD_MAX_SIZE] = {0};
|
|
size_t digest_len = mbedtls_md_get_size(md_info);
|
|
if (digest_len == 0 || digest_len > sizeof(digest) ||
|
|
mbedtls_md(md_info, cert->tbs.p, cert->tbs.len, digest) != 0) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
mbedtls_ecdsa_context ctx;
|
|
mbedtls_ecdsa_init(&ctx);
|
|
int status = PM3_ESOFT;
|
|
if (mbedtls_ecp_group_load(&ctx.grp, signer_curveid) != 0) {
|
|
goto out;
|
|
}
|
|
size_t expected_key_len = 1 + 2 * ((ctx.grp.nbits + 7) / 8);
|
|
if (signer_pubkey_len != expected_key_len ||
|
|
mbedtls_ecp_point_read_binary(&ctx.grp, &ctx.Q, signer_pubkey, signer_pubkey_len) != 0 ||
|
|
mbedtls_ecp_check_pubkey(&ctx.grp, &ctx.Q) != 0) {
|
|
status = PM3_EINVARG;
|
|
goto out;
|
|
}
|
|
|
|
status = (mbedtls_ecdsa_read_signature(&ctx, digest, digest_len, cert->sig.p, cert->sig.len) == 0) ? PM3_SUCCESS : PM3_ESOFT;
|
|
|
|
out:
|
|
mbedtls_ecdsa_free(&ctx);
|
|
return status;
|
|
}
|
|
|
|
static int duox_verify_x509_signature_with_anchor(const mbedtls_x509_crt *cert,
|
|
const duox_certificate_anchor_t *anchor,
|
|
bool verbose) {
|
|
if (cert == NULL || anchor == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
mbedtls_ecp_group_id curveid = MBEDTLS_ECP_DP_NONE;
|
|
const uint8_t *pubkey = NULL;
|
|
size_t pubkey_len = 0;
|
|
int res = duox_certificate_anchor_public_key(anchor, &curveid, &pubkey, &pubkey_len);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
|
|
return duox_verify_x509_signature_with_public_key(cert, curveid, pubkey, pubkey_len, verbose);
|
|
}
|
|
|
|
int duox_verify_x509_certificate_with_anchors(const uint8_t *data, size_t data_len,
|
|
const duox_certificate_anchor_t *anchors, size_t anchor_count,
|
|
bool verbose, duox_cert_info_t *out,
|
|
size_t *matched_index) {
|
|
if (data == NULL || data_len == 0 || anchors == NULL || anchor_count == 0 ||
|
|
anchor_count > DUOX_MAX_CERTIFICATE_ANCHORS || out == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
mbedtls_x509_crt cert;
|
|
mbedtls_x509_crt_init(&cert);
|
|
int res = duox_parse_x509_der_crt(data, data_len, verbose, &cert);
|
|
if (res != PM3_SUCCESS) {
|
|
mbedtls_x509_crt_free(&cert);
|
|
return res;
|
|
}
|
|
|
|
duox_cert_info_t info = {0};
|
|
res = duox_cert_info_from_x509_crt(&cert, &info);
|
|
*out = info;
|
|
if (res != PM3_SUCCESS) {
|
|
mbedtls_x509_crt_free(&cert);
|
|
return PM3_ECRYPTO;
|
|
}
|
|
|
|
bool tried[DUOX_MAX_CERTIFICATE_ANCHORS] = {0};
|
|
int verify_res = PM3_ECRYPTO;
|
|
if (info.issuer[0] != '\0') {
|
|
for (size_t i = 0; i < anchor_count; i++) {
|
|
const char *subject = duox_certificate_anchor_subject(&anchors[i]);
|
|
if (subject[0] == '\0' || !str_contains_case_insensitive(info.issuer, subject)) {
|
|
continue;
|
|
}
|
|
|
|
tried[i] = true;
|
|
if (duox_verify_x509_signature_with_anchor(&cert, &anchors[i], verbose) == PM3_SUCCESS) {
|
|
if (matched_index != NULL) {
|
|
*matched_index = i;
|
|
}
|
|
verify_res = PM3_SUCCESS;
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < anchor_count; i++) {
|
|
if (tried[i]) {
|
|
continue;
|
|
}
|
|
|
|
if (duox_verify_x509_signature_with_anchor(&cert, &anchors[i], verbose) == PM3_SUCCESS) {
|
|
if (matched_index != NULL) {
|
|
*matched_index = i;
|
|
}
|
|
verify_res = PM3_SUCCESS;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
out:
|
|
mbedtls_x509_crt_free(&cert);
|
|
return verify_res;
|
|
}
|
|
|
|
static bool duox_format_bcd_date(const uint8_t *data, size_t data_len, char *out, size_t out_len) {
|
|
if (data == NULL || data_len != 4 || out == NULL || out_len < 11) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < data_len; i++) {
|
|
if ((data[i] >> 4) > 9 || (data[i] & 0x0F) > 9) {
|
|
return false;
|
|
}
|
|
}
|
|
snprintf(out, out_len, "%02X%02X-%02X-%02X", data[0], data[1], data[2], data[3]);
|
|
return true;
|
|
}
|
|
|
|
typedef struct {
|
|
uint32_t tag;
|
|
size_t tl_offset;
|
|
size_t value_offset;
|
|
size_t len;
|
|
size_t total_len;
|
|
const uint8_t *value;
|
|
} duox_gp_vde_tlv_t;
|
|
|
|
static int duox_gp_vde_parse_tlv(const uint8_t *data, size_t data_len, size_t *offset, duox_gp_vde_tlv_t *tlv) {
|
|
if (data == NULL || offset == NULL || tlv == NULL || *offset >= data_len) {
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
size_t idx = *offset;
|
|
uint32_t tag = data[idx++];
|
|
if ((tag & 0x1F) == 0x1F) {
|
|
do {
|
|
if (idx >= data_len || tag > 0x00FFFFFFU) {
|
|
return PM3_ENODATA;
|
|
}
|
|
uint8_t b = data[idx++];
|
|
tag = (tag << 8) | b;
|
|
if ((b & 0x80) == 0) {
|
|
break;
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
size_t len = 0;
|
|
if (idx >= data_len || data[idx] == 0x80 ||
|
|
((data[idx] & 0x80) != 0 && (data[idx] & 0x7F) > sizeof(size_t))) {
|
|
return PM3_ENODATA;
|
|
}
|
|
if (asn1_get_tag_length(data, &len, &idx, data_len) != 0 || len > data_len - idx) {
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
tlv->tag = tag;
|
|
tlv->tl_offset = *offset;
|
|
tlv->value_offset = idx;
|
|
tlv->len = len;
|
|
tlv->total_len = (idx - *offset) + len;
|
|
tlv->value = data + idx;
|
|
*offset = idx + len;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int duox_verify_gp_vde_signature(const uint8_t *signed_data, size_t signed_data_len,
|
|
const uint8_t signature_rs[DUOX_VDE_SIG_LEN],
|
|
const uint8_t *ca_id, size_t ca_id_len,
|
|
const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count,
|
|
bool verbose, size_t *matched_index) {
|
|
if (signed_data == NULL || signed_data_len == 0 || signature_rs == NULL ||
|
|
ca_anchors == NULL || ca_anchor_count == 0 || ca_anchor_count > DUOX_MAX_CERTIFICATE_ANCHORS) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
char ca_id_hex[32] = {0};
|
|
snprintf(ca_id_hex, sizeof(ca_id_hex), "%s", sprint_hex_inrow(ca_id, ca_id_len));
|
|
|
|
bool tried[DUOX_MAX_CERTIFICATE_ANCHORS] = {0};
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
for (size_t i = 0; i < ca_anchor_count; i++) {
|
|
if (tried[i]) {
|
|
continue;
|
|
}
|
|
if (pass == 0 && ca_id_hex[0] != '\0' &&
|
|
strstr(duox_certificate_anchor_subject(&ca_anchors[i]), ca_id_hex) == NULL) {
|
|
continue;
|
|
}
|
|
|
|
mbedtls_ecp_group_id curveid = MBEDTLS_ECP_DP_NONE;
|
|
const uint8_t *pubkey = NULL;
|
|
size_t pubkey_len = 0;
|
|
if (duox_certificate_anchor_public_key(&ca_anchors[i], &curveid, &pubkey, &pubkey_len) != PM3_SUCCESS ||
|
|
curveid != MBEDTLS_ECP_DP_BP256R1 || pubkey_len != 65) {
|
|
continue;
|
|
}
|
|
|
|
tried[i] = true;
|
|
int res = ecdsa_signature_r_s_verify(MBEDTLS_ECP_DP_BP256R1,
|
|
(uint8_t *)pubkey,
|
|
(uint8_t *)signed_data,
|
|
(int)signed_data_len,
|
|
(uint8_t *)signature_rs,
|
|
DUOX_VDE_SIG_LEN,
|
|
true);
|
|
if (res == PM3_SUCCESS) {
|
|
if (matched_index != NULL) {
|
|
*matched_index = i;
|
|
}
|
|
return PM3_SUCCESS;
|
|
}
|
|
if (verbose) {
|
|
PrintAndLogEx(INFO, "GP VDE signature did not verify with %s", duox_certificate_anchor_display_name(&ca_anchors[i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
return PM3_ECRYPTO;
|
|
}
|
|
|
|
int duox_parse_gp_vde_certificate(const uint8_t *data, size_t data_len,
|
|
const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count,
|
|
bool verify_signature,
|
|
bool verbose, duox_cert_info_t *out, size_t *matched_index) {
|
|
if (data == NULL || data_len == 0 || out == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (verify_signature && (ca_anchors == NULL || ca_anchor_count == 0)) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
size_t offset = 0;
|
|
duox_gp_vde_tlv_t outer = {0};
|
|
int res = duox_gp_vde_parse_tlv(data, data_len, &offset, &outer);
|
|
if (res != PM3_SUCCESS || outer.tag != 0x7F21) {
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
size_t cert_end = outer.value_offset + outer.len;
|
|
const uint8_t *ca_id = NULL;
|
|
size_t ca_id_len = 0;
|
|
const uint8_t *serial_uid = NULL;
|
|
size_t serial_uid_len = 0;
|
|
const uint8_t *subject_id = NULL;
|
|
size_t subject_id_len = 0;
|
|
const uint8_t *signature_rs = NULL;
|
|
const uint8_t *public_key = NULL;
|
|
size_t signed_start = SIZE_MAX;
|
|
size_t signed_end = 0;
|
|
bool curve_ok = false;
|
|
char valid_from[DUOX_CERT_TEXT_LEN] = {0};
|
|
char valid_to[DUOX_CERT_TEXT_LEN] = {0};
|
|
|
|
offset = outer.value_offset;
|
|
while (offset < cert_end) {
|
|
duox_gp_vde_tlv_t tlv = {0};
|
|
res = duox_gp_vde_parse_tlv(data, cert_end, &offset, &tlv);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
|
|
switch (tlv.tag) {
|
|
case 0x93:
|
|
serial_uid = tlv.value;
|
|
serial_uid_len = tlv.len;
|
|
signed_start = tlv.tl_offset;
|
|
break;
|
|
case 0x42:
|
|
ca_id = tlv.value;
|
|
ca_id_len = tlv.len;
|
|
break;
|
|
case 0x5F20:
|
|
subject_id = tlv.value;
|
|
subject_id_len = tlv.len;
|
|
break;
|
|
case 0x5F25:
|
|
duox_format_bcd_date(tlv.value, tlv.len, valid_from, sizeof(valid_from));
|
|
break;
|
|
case 0x5F24:
|
|
duox_format_bcd_date(tlv.value, tlv.len, valid_to, sizeof(valid_to));
|
|
break;
|
|
case 0x7F49: {
|
|
size_t pki_end = tlv.value_offset + tlv.len;
|
|
size_t pki_offset = tlv.value_offset;
|
|
while (pki_offset < pki_end) {
|
|
duox_gp_vde_tlv_t ktlv = {0};
|
|
res = duox_gp_vde_parse_tlv(data, pki_end, &pki_offset, &ktlv);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
if (ktlv.tag == 0xB0 && ktlv.len == 65 && ktlv.value[0] == 0x04) {
|
|
public_key = ktlv.value;
|
|
} else if (ktlv.tag == 0xF0 && ktlv.len == 1 && ktlv.value[0] == 0x03) {
|
|
curve_ok = true;
|
|
}
|
|
}
|
|
signed_end = tlv.tl_offset + tlv.total_len;
|
|
break;
|
|
}
|
|
case 0x5F37:
|
|
if (tlv.len == DUOX_VDE_SIG_LEN) {
|
|
signature_rs = tlv.value;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (signed_start == SIZE_MAX || signed_end <= signed_start || public_key == NULL || !curve_ok ||
|
|
signature_rs == NULL || ca_id == NULL || subject_id == NULL) {
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
duox_cert_info_t info = {0};
|
|
info.format = DUOX_CERTIFICATE_FORMAT_GP_VDE;
|
|
info.curveid = MBEDTLS_ECP_DP_BP256R1;
|
|
memcpy(info.pubkey, public_key, 65);
|
|
info.pubkey_len = 65;
|
|
|
|
char ca_id_hex[32] = {0};
|
|
char serial_hex[32] = {0};
|
|
char subject_hex[32] = {0};
|
|
snprintf(ca_id_hex, sizeof(ca_id_hex), "%s", sprint_hex_inrow(ca_id, ca_id_len));
|
|
snprintf(serial_hex, sizeof(serial_hex), "%s", sprint_hex_inrow(serial_uid, serial_uid_len));
|
|
snprintf(subject_hex, sizeof(subject_hex), "%s", sprint_hex_inrow(subject_id, subject_id_len));
|
|
snprintf(info.issuer, sizeof(info.issuer), "CA ID %s", ca_id_hex);
|
|
snprintf(info.subject, sizeof(info.subject), "Subject ID %s", subject_hex);
|
|
snprintf(info.serial, sizeof(info.serial), "%s", serial_hex);
|
|
snprintf(info.valid_from, sizeof(info.valid_from), "%s", valid_from);
|
|
snprintf(info.valid_to, sizeof(info.valid_to), "%s", valid_to);
|
|
snprintf(info.certificate_profile_note, sizeof(info.certificate_profile_note), "VDE CA ID %s", ca_id_hex);
|
|
*out = info;
|
|
|
|
if (!verify_signature) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
return duox_verify_gp_vde_signature(data + signed_start, signed_end - signed_start,
|
|
signature_rs, ca_id, ca_id_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verbose, matched_index);
|
|
}
|
|
|
|
static int duox_parse_or_verify_x509_payload(const uint8_t *data, size_t data_len,
|
|
const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count,
|
|
bool verify_signature,
|
|
bool verbose, duox_cert_info_t *out, size_t *matched_index) {
|
|
return verify_signature
|
|
? duox_verify_x509_certificate_with_anchors(data, data_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verbose, out, matched_index)
|
|
: duox_parse_x509_certificate(data, data_len, verbose, out);
|
|
}
|
|
|
|
static int duox_verify_x509_certificate_variants(const uint8_t *data, size_t data_len,
|
|
const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count,
|
|
bool verify_signature,
|
|
bool verbose, duox_cert_info_t *out, size_t *matched_index) {
|
|
if (data == NULL || data_len == 0 || out == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
int res = duox_parse_or_verify_x509_payload(data, data_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verify_signature,
|
|
verbose, out, matched_index);
|
|
if (res != PM3_ENODATA) {
|
|
return res;
|
|
}
|
|
|
|
size_t payload_len = 0;
|
|
if (data_len > 3) {
|
|
payload_len = (size_t)data[0] | ((size_t)data[1] << 8) | ((size_t)data[2] << 16);
|
|
if (payload_len > 0 && payload_len <= data_len - 3) {
|
|
if (verbose) {
|
|
PrintAndLogEx(INFO, "Trying 3-byte length-prefixed X.509 payload (%zu bytes)", payload_len);
|
|
}
|
|
res = duox_parse_or_verify_x509_payload(data + 3, payload_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verify_signature,
|
|
verbose, out, matched_index);
|
|
if (res != PM3_ENODATA) {
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data_len > 2 && data[0] == 0x30) {
|
|
size_t asn1_len = 0;
|
|
size_t asn1_offset = 1;
|
|
if (asn1_get_tag_length(data, &asn1_len, &asn1_offset, data_len) == 0 &&
|
|
asn1_len <= data_len - asn1_offset) {
|
|
payload_len = asn1_offset + asn1_len;
|
|
}
|
|
}
|
|
if (payload_len > 0 && payload_len < data_len) {
|
|
if (verbose) {
|
|
PrintAndLogEx(INFO, "Trying zero-padded X.509 payload (%zu bytes)", payload_len);
|
|
}
|
|
return duox_parse_or_verify_x509_payload(data, payload_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verify_signature,
|
|
verbose, out, matched_index);
|
|
}
|
|
|
|
return PM3_ENODATA;
|
|
}
|
|
|
|
int duox_parse_or_verify_certificate_variants(const uint8_t *data, size_t data_len,
|
|
const duox_certificate_anchor_t *ca_anchors, size_t ca_anchor_count,
|
|
bool verify_signature,
|
|
bool verbose, duox_cert_info_t *out, size_t *matched_index) {
|
|
int x509_res = duox_verify_x509_certificate_variants(data, data_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verify_signature,
|
|
verbose, out, matched_index);
|
|
if (x509_res != PM3_ENODATA) {
|
|
return x509_res;
|
|
}
|
|
|
|
int gp_res = duox_parse_gp_vde_certificate(data, data_len,
|
|
ca_anchors, ca_anchor_count,
|
|
verify_signature,
|
|
verbose, out, matched_index);
|
|
if (gp_res != PM3_ENODATA) {
|
|
return gp_res;
|
|
}
|
|
|
|
return PM3_ENODATA;
|
|
}
|