Files
proxmark3/client/src/crypto/duoxcrypto.c
T
2026-05-11 14:36:17 +02:00

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;
}