diff --git a/src/p11_eddsa.c b/src/p11_eddsa.c index 67a0e521..5ca5be7f 100644 --- a/src/p11_eddsa.c +++ b/src/p11_eddsa.c @@ -330,78 +330,251 @@ void pkcs11_ed_key_method_free(void) #endif /* OPENSSL_VERSION_NUMBER < 0x40000000L */ /* - * Retrieve the raw public key (EdDSA) from a PKCS#11 object. - * The buffer `*raw` is allocated and must be freed by the caller - * using OPENSSL_free(). + * Decode a DER OCTET STRING and return its contents. + * If decoding fails, duplicate the input buffer unchanged. + * Returns 1 on success, 0 on error. */ -static int pkcs11_get_raw_public_key(PKCS11_OBJECT_private *key, +static int strip_der_octet_string_alloc(const unsigned char *in, size_t inlen, + unsigned char **out, size_t *outlen) +{ + const unsigned char *p = in; + unsigned char *buf; + ASN1_OCTET_STRING *os = NULL; + + if (out == NULL || outlen == NULL || in == NULL || inlen == 0) + return 0; + + *out = NULL; + *outlen = 0; + + /* For EdDSA (RFC8032) the CKA_EC_POINT attribute may be + * encoded as a DER OCTET STRING */ + os = d2i_ASN1_OCTET_STRING(NULL, &p, (long)inlen); + if (os != NULL && p == in + inlen) { + buf = OPENSSL_malloc((size_t)os->length); + if (buf == NULL) { + ASN1_OCTET_STRING_free(os); + return 0; + } + memcpy(buf, os->data, (size_t)os->length); + *out = buf; + *outlen = (size_t)os->length; + ASN1_OCTET_STRING_free(os); + return 1; + } + ASN1_OCTET_STRING_free(os); + + /* Not DER OCTET STRING -> copy as-is */ + buf = OPENSSL_malloc(inlen); + if (buf == NULL) + return 0; + memcpy(buf, in, inlen); + *out = buf; + *outlen = inlen; + return 1; +} + +/* + * Parse a DER X.509 certificate and return raw Ed25519/Ed448 public key bytes. + * Returns 1 on success, 0 on error. + */ +static int extract_eddsa_raw_from_x509_der(const unsigned char *der, size_t derlen, unsigned char **raw, size_t *rawlen) { - CK_ATTRIBUTE attr; - CK_RV rv; - CK_SESSION_HANDLE session; - CK_MECHANISM mechanism; - PKCS11_SLOT_private *slot = key->slot; - PKCS11_CTX_private *ctx = slot->ctx; - PKCS11_OBJECT_private *pubkey; + const unsigned char *p = der; + X509 *cert = NULL; + EVP_PKEY *pkey = NULL; + unsigned char *buf = NULL; + size_t len = 0; + int type, ok = 0; + + if (raw == NULL || rawlen == NULL || der == NULL || derlen == 0) + return 0; *raw = NULL; *rawlen = 0; - memset(&mechanism, 0, sizeof(mechanism)); - mechanism.mechanism = CKM_EDDSA; + cert = d2i_X509(NULL, &p, (long)derlen); + if (cert == NULL) + goto end; - memset(&attr, 0, sizeof(attr)); + pkey = X509_get_pubkey(cert); + if (pkey == NULL) + goto end; - /* CKA_EC_POINT: DER-encoding of the b-bit public key value - * in little endian order as defined in RFC 8032 */ - attr.type = CKA_EC_POINT; + /* Ensure it's EdDSA (Ed25519/Ed448) */ + type = EVP_PKEY_id(pkey); + if (type != EVP_PKEY_ED25519 && type != EVP_PKEY_ED448) + goto end; - if (pkcs11_get_session(slot, 0, &session)) - return -1; - if (key->object_class == CKO_PRIVATE_KEY) - pubkey = pkcs11_object_from_object(key, session, CKO_PUBLIC_KEY); - else - pubkey = key; + /* Query required size */ + if (EVP_PKEY_get_raw_public_key(pkey, NULL, &len) != 1 || len == 0) + goto end; - rv = CRYPTOKI_call(ctx, C_GetAttributeValue(session, pubkey->object, &attr, 1)); - if (rv != CKR_OK) - return -1; + buf = OPENSSL_malloc(len); + if (buf == NULL) + goto end; - if (attr.ulValueLen <= 0 || attr.ulValueLen == CK_UNAVAILABLE_INFORMATION) - return -1; + /* Fetch raw public key bytes */ + if (EVP_PKEY_get_raw_public_key(pkey, buf, &len) != 1) { + OPENSSL_free(buf); + buf = NULL; + goto end; + } + + *raw = buf; + *rawlen = len; + buf = NULL; + ok = 1; + +end: + OPENSSL_free(buf); + EVP_PKEY_free(pkey); + X509_free(cert); + return ok; +} - *raw = OPENSSL_malloc(attr.ulValueLen); - if (!*raw) + +/* + * Extract raw EdDSA public key bytes from a CKO_PUBLIC_KEY object using CKA_EC_POINT. + * Returns 1 on success, 0 on failure. + */ +static int extract_pub_from_public_key_obj(PKCS11_CTX_private *ctx, CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE obj, unsigned char **raw, size_t *rawlen) +{ + unsigned char *ecpt = NULL; + size_t ecptlen = 0; + int ok = 0; + + if (pkcs11_getattr_alloc(ctx, session, obj, CKA_EC_POINT, &ecpt, &ecptlen)) + return 0; + + ok = strip_der_octet_string_alloc(ecpt, ecptlen, raw, rawlen); + OPENSSL_free(ecpt); + return ok; +} + +/* + * Extract raw EdDSA public key bytes from a CKO_CERTIFICATE object. + * The certificate is read from CKA_VALUE (DER-encoded X.509) and + * the public key is obtained via X.509 parsing. + * Returns 1 on success, 0 on failure. + */ +static int extract_pub_from_cert_obj(PKCS11_CTX_private *ctx, CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE obj, unsigned char **raw, size_t *rawlen) +{ + unsigned char *der = NULL; + size_t derlen = 0; + int ok = 0; + + if (pkcs11_getattr_alloc(ctx, session, obj, CKA_VALUE, &der, &derlen)) + return 0; + + ok = extract_eddsa_raw_from_x509_der(der, derlen, raw, rawlen); + OPENSSL_free(der); + return ok; +} + +/* + * Select an object that can provide public key material. + * + * For a private key object, try to locate a matching CKO_PUBLIC_KEY + * (same CKA_ID). If not found, fall back to CKO_CERTIFICATE. + * + * On success, returns a PKCS11_OBJECT_private pointer. + * If a new object is returned, *needs_free is set to 1 and the caller + * must free it with pkcs11_object_free(). + * + * Returns NULL on failure. + */ +static PKCS11_OBJECT_private *pkcs11_choose_public_source(PKCS11_OBJECT_private *key, + CK_SESSION_HANDLE session, int *needs_free) +{ + PKCS11_OBJECT_private *obj; + + *needs_free = 0; + + if (key->object_class != CKO_PRIVATE_KEY) + return key; + + obj = pkcs11_object_from_object(key, session, CKO_PUBLIC_KEY); + if (obj != NULL && obj->object != CK_INVALID_HANDLE) { + *needs_free = 1; + return obj; + } + if (obj != NULL) + pkcs11_object_free(obj); + + obj = pkcs11_object_from_object(key, session, CKO_CERTIFICATE); + if (obj != NULL && obj->object != CK_INVALID_HANDLE) { + *needs_free = 1; + return obj; + } + if (obj != NULL) + pkcs11_object_free(obj); + + return NULL; +} + +/* + * Retrieve raw EdDSA public key bytes. + * + * Preference order: + * 1. CKO_PUBLIC_KEY -> CKA_EC_POINT + * 2. CKO_CERTIFICATE -> CKA_VALUE (DER) + X.509 parsing + * + * The returned buffer is allocated with OPENSSL_malloc() + * and must be freed by the caller. + * + * Returns 0 on success, -1 on failure. + */ +static int pkcs11_get_raw_public_key(PKCS11_OBJECT_private *key, + unsigned char **raw, size_t *rawlen) +{ + PKCS11_SLOT_private *slot; + PKCS11_CTX_private *ctx; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + PKCS11_OBJECT_private *obj = NULL; + int obj_needs_free = 0, ok = 0; + + if (key == NULL || key->slot == NULL || raw == NULL || rawlen == NULL) return -1; - attr.pValue = *raw; + *raw = NULL; + *rawlen = 0; + slot = key->slot; + ctx = slot->ctx; - rv = CRYPTOKI_call(ctx, C_GetAttributeValue(session, pubkey->object, &attr, 1)); + if (pkcs11_get_session(slot, 0, &session)) + return -1; - if (key->object_class == CKO_PRIVATE_KEY) - pkcs11_object_free(pubkey); + obj = pkcs11_choose_public_source(key, session, &obj_needs_free); + if (obj == NULL || obj->object == CK_INVALID_HANDLE) + goto end; + + switch (obj->object_class) { + case CKO_PUBLIC_KEY: + ok = extract_pub_from_public_key_obj(ctx, session, obj->object, raw, rawlen); + break; + case CKO_CERTIFICATE: + ok = extract_pub_from_cert_obj(ctx, session, obj->object, raw, rawlen); + break; + default: + ok = 0; + break; + } - if (rv != CKR_OK) { +end: + if (!ok) { OPENSSL_free(*raw); *raw = NULL; - return -1; + *rawlen = 0; } - *rawlen = attr.ulValueLen; - - /* For EdDSA (RFC8032) the CKA_EC_POINT attribute may be encoded - * as a DER OCTET STRING. In such a case, the ASN.1 header needs - * to be stripped, leaving only the raw key bytes. */ - if (*rawlen > 2 && (*raw)[0] == 0x04) { - /* simple OCTET STRING parser */ - size_t len = (*raw)[1]; - if (len + 2 == *rawlen) { - memmove(*raw, *raw + 2, len); - *rawlen = len; - } - } - return 0; + if (obj_needs_free && obj != NULL) + pkcs11_object_free(obj); + + return ok ? 0 : -1; } static EVP_PKEY *pkcs11_get_evp_key_ed25519(PKCS11_OBJECT_private *key) diff --git a/src/p11_key.c b/src/p11_key.c index dd328e29..c8a2ba07 100644 --- a/src/p11_key.c +++ b/src/p11_key.c @@ -125,8 +125,8 @@ static CK_OBJECT_HANDLE pkcs11_handle_from_template(PKCS11_SLOT_private *slot, CK_SESSION_HANDLE session, PKCS11_TEMPLATE *tmpl) { PKCS11_CTX_private *ctx = slot->ctx; - CK_OBJECT_HANDLE object; - CK_ULONG count; + CK_OBJECT_HANDLE object = CK_INVALID_HANDLE; + CK_ULONG count = 0; CK_RV rv; rv = CRYPTOKI_call(ctx, diff --git a/tests/Makefile.am b/tests/Makefile.am index b18ddaec..208309c4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -65,6 +65,8 @@ dist_check_SCRIPTS = \ provider-ec-check-privkey.softhsm \ provider-ec-check-all.softhsm \ provider-ec-copy.softhsm \ + provider-ed25519-check-privkey.softhsm \ + provider-ed448-check-privkey.softhsm \ provider-ed25519-keygen.softhsm \ provider-ed448-keygen.softhsm \ provider-fork-change-slot.softhsm \ @@ -73,7 +75,9 @@ dist_check_SCRIPTS = \ provider-search-all-matching-tokens.softhsm dist_check_DATA = \ rsa-cert.der rsa-privkey.der rsa-pubkey.der \ - ec-cert.der ec-privkey.der ec-pubkey.der + ec-cert.der ec-privkey.der ec-pubkey.der \ + ed25519-cert.der ed25519-privkey.der ed25519-pubkey.der \ + ed448-cert.der ed448-privkey.der ed448-pubkey.der ed25519_keygen_SOURCES = ed25519-keygen.c eddsa_common.c ed448_keygen_SOURCES = ed448-keygen.c eddsa_common.c diff --git a/tests/check-privkey-prov.c b/tests/check-privkey-prov.c index 8c5096b6..63cda8ad 100644 --- a/tests/check-privkey-prov.c +++ b/tests/check-privkey-prov.c @@ -60,7 +60,7 @@ int main(int argc, char *argv[]) /* Load private key */ private_key = load_pkey(argv[2], NULL); if (!private_key) { - fprintf(stderr, "Cannot load private key: %s\n", argv[1]); + fprintf(stderr, "Cannot load private key: %s\n", argv[2]); display_openssl_errors(); goto cleanup; } diff --git a/tests/common.sh b/tests/common.sh index c2dcee54..e920c1b5 100755 --- a/tests/common.sh +++ b/tests/common.sh @@ -244,10 +244,15 @@ init_token () { import_objects ${key_type} "${common_label}-$i" ${obj_id} "${obj_label}-$i" "$@" # List the objects imported into the token - list_objects "${common_label}-$i" + if [[ $? -eq 0 ]]; then + list_objects "${common_label}-$i" + else + return 77 + fi i=$(($i + 1)) done + return 0 } # Write an object (privkey, pubkey, cert) to the token @@ -272,10 +277,10 @@ import_objects () { --id ${obj_id} --label "${obj_label}" >/dev/null if [[ $? -eq 0 ]]; then echo ok - else - echo failed - exit 1 + continue fi + echo "pkcs11-tool cannot import ${key_type} ${param}" + return 77 else echo "Skipping empty parameter" fi diff --git a/tests/ed25519-cert.der b/tests/ed25519-cert.der new file mode 100644 index 00000000..5815cac6 Binary files /dev/null and b/tests/ed25519-cert.der differ diff --git a/tests/ed25519-privkey.der b/tests/ed25519-privkey.der new file mode 100644 index 00000000..81c132fa Binary files /dev/null and b/tests/ed25519-privkey.der differ diff --git a/tests/ed25519-pubkey.der b/tests/ed25519-pubkey.der new file mode 100644 index 00000000..9601592e Binary files /dev/null and b/tests/ed25519-pubkey.der differ diff --git a/tests/ed448-cert.der b/tests/ed448-cert.der new file mode 100644 index 00000000..ab3d2316 Binary files /dev/null and b/tests/ed448-cert.der differ diff --git a/tests/ed448-privkey.der b/tests/ed448-privkey.der new file mode 100644 index 00000000..9ec0984b Binary files /dev/null and b/tests/ed448-privkey.der differ diff --git a/tests/ed448-pubkey.der b/tests/ed448-pubkey.der new file mode 100644 index 00000000..66432607 Binary files /dev/null and b/tests/ed448-pubkey.der differ diff --git a/tests/provider-ec-check-privkey.softhsm b/tests/provider-ec-check-privkey.softhsm index 30b82e77..d71af96b 100755 --- a/tests/provider-ec-check-privkey.softhsm +++ b/tests/provider-ec-check-privkey.softhsm @@ -45,8 +45,7 @@ ${OPENSSL} x509 -in ${srcdir}/ec-cert.der -inform DER -outform PEM \ CERTIFICATE="${outdir}/ec-cert.pem" # Run the test -${WRAPPER} ./check-privkey-prov ${CERTIFICATE} ${PRIVATE_KEY} \ - "${outdir}/engines.cnf" +${WRAPPER} ./check-privkey-prov ${CERTIFICATE} ${PRIVATE_KEY} rc=$? if [[ $rc -eq 77 ]]; then echo "EC key test skipped." @@ -57,8 +56,7 @@ elif [[ $rc -ne 0 ]]; then exit 1 fi -./check-privkey-prov ${CERTIFICATE_URL} ${PRIVATE_KEY} \ - "${outdir}/engines.cnf" +./check-privkey-prov ${CERTIFICATE_URL} ${PRIVATE_KEY} if [[ $? -ne 0 ]]; then echo "The private key loading couldn't get the public key from the certificate URL." exit 1 diff --git a/tests/provider-ed25519-check-privkey.softhsm b/tests/provider-ed25519-check-privkey.softhsm new file mode 100755 index 00000000..8f3415fe --- /dev/null +++ b/tests/provider-ed25519-check-privkey.softhsm @@ -0,0 +1,81 @@ +#!/bin/bash + +# Copyright © 2026 Mobi - Com Polska Sp. z o.o. +# Author: Małgorzata Olszówka +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# +# EdDSA public key resolution tests +# +# The provider resolves the public key for a private key as follows: +# 1. CKO_PUBLIC_KEY (CKA_EC_POINT) +# 2. CKO_CERTIFICATE (CKA_VALUE + X.509 parsing) +# +# Ed25519: +# Import privkey + cert only (no pubkey) to exercise the CKO_CERTIFICATE fallback path. + +outdir="output.$$" + +# Load common test functions +. ${srcdir}/common.sh + +PRIVATE_KEY="pkcs11:token=libp11-0;id=%01%02%03%04;object=server-key-0;type=private;pin-value=${PIN}" +CERTIFICATE_URL="pkcs11:token=libp11-0;id=%01%02%03%04;object=server-key-0;type=cert" + +# Do the token initialization +init_token "ed25519" "1" "libp11" ${ID} "server-key" "privkey" "" "cert" +if [[ $? -eq 77 ]]; then + echo "Ed448 key test skipped." + rm -rf "$outdir" + exit 77 +fi + +# Ensure the use of the locally built provider; applies after running 'pkcs11-tool' +unset OPENSSL_ENGINES +export OPENSSL_MODULES="../src/.libs/" +export PKCS11_MODULE_PATH=${MODULE} +echo "OPENSSL_MODULES=${OPENSSL_MODULES}" +echo "PKCS11_MODULE_PATH=${PKCS11_MODULE_PATH}" + +# Load openssl settings +. ${srcdir}/openssl-settings.sh + +# Restore openssl settings +trap cleanup EXIT + +${OPENSSL} x509 -in ${srcdir}/ed25519-cert.der -inform DER -outform PEM \ + -out ${outdir}/ed25519-cert.pem +CERTIFICATE="${outdir}/ed25519-cert.pem" + +# Run the test +${WRAPPER} ./check-privkey-prov ${CERTIFICATE} ${PRIVATE_KEY} +rc=$? +if [[ $rc -eq 77 ]]; then + echo "Ed25519 key test skipped." + rm -rf "$outdir" + exit 77 +elif [[ $rc -ne 0 ]]; then + echo "The private key loading couldn't get the public key from the certificate." + exit 1 +fi + +./check-privkey-prov ${CERTIFICATE_URL} ${PRIVATE_KEY} +if [[ $? -ne 0 ]]; then + echo "The private key loading couldn't get the public key from the certificate URL." + exit 1 +fi + +rm -rf "$outdir" + +exit 0 diff --git a/tests/provider-ed448-check-privkey.softhsm b/tests/provider-ed448-check-privkey.softhsm new file mode 100755 index 00000000..33730cad --- /dev/null +++ b/tests/provider-ed448-check-privkey.softhsm @@ -0,0 +1,81 @@ +#!/bin/bash + +# Copyright © 2026 Mobi - Com Polska Sp. z o.o. +# Author: Małgorzata Olszówka +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# +# EdDSA public key resolution tests +# +# The provider resolves the public key for a private key as follows: +# 1. CKO_PUBLIC_KEY (CKA_EC_POINT) +# 2. CKO_CERTIFICATE (CKA_VALUE + X.509 parsing) +# +# Ed448: +# Import privkey + pubkey + cert to exercise the direct CKO_PUBLIC_KEY lookup path. + +outdir="output.$$" + +# Load common test functions +. ${srcdir}/common.sh + +PRIVATE_KEY="pkcs11:token=libp11-0;id=%01%02%03%04;object=server-key-0;type=private;pin-value=${PIN}" +CERTIFICATE_URL="pkcs11:token=libp11-0;id=%01%02%03%04;object=server-key-0;type=cert" + +# Do the token initialization +init_token "ed448" "1" "libp11" ${ID} "server-key" "privkey" "pubkey" "cert" +if [[ $? -eq 77 ]]; then + echo "Ed448 key test skipped." + rm -rf "$outdir" + exit 77 +fi + +# Ensure the use of the locally built provider; applies after running 'pkcs11-tool' +unset OPENSSL_ENGINES +export OPENSSL_MODULES="../src/.libs/" +export PKCS11_MODULE_PATH=${MODULE} +echo "OPENSSL_MODULES=${OPENSSL_MODULES}" +echo "PKCS11_MODULE_PATH=${PKCS11_MODULE_PATH}" + +# Load openssl settings +. ${srcdir}/openssl-settings.sh + +# Restore openssl settings +trap cleanup EXIT + +${OPENSSL} x509 -in ${srcdir}/ed448-cert.der -inform DER -outform PEM \ + -out ${outdir}/ed448-cert.pem +CERTIFICATE="${outdir}/ed448-cert.pem" + +# Run the test +${WRAPPER} ./check-privkey-prov ${CERTIFICATE} ${PRIVATE_KEY} +rc=$? +if [[ $rc -eq 77 ]]; then + echo "Ed448 key test skipped." + rm -rf "$outdir" + exit 77 +elif [[ $rc -ne 0 ]]; then + echo "The private key loading couldn't get the public key from the certificate." + exit 1 +fi + +./check-privkey-prov ${CERTIFICATE_URL} ${PRIVATE_KEY} +if [[ $? -ne 0 ]]; then + echo "The private key loading couldn't get the public key from the certificate URL." + exit 1 +fi + +rm -rf "$outdir" + +exit 0 diff --git a/tests/provider-rsa-check-privkey.softhsm b/tests/provider-rsa-check-privkey.softhsm index 9f29d6df..2e32f1e7 100755 --- a/tests/provider-rsa-check-privkey.softhsm +++ b/tests/provider-rsa-check-privkey.softhsm @@ -45,8 +45,7 @@ ${OPENSSL} x509 -in ${srcdir}/rsa-cert.der -inform DER -outform PEM \ CERTIFICATE="${outdir}/rsa-cert.pem" # Run the test -${WRAPPER} ./check-privkey-prov ${CERTIFICATE} ${PRIVATE_KEY} \ - "${outdir}/engines.cnf" +${WRAPPER} ./check-privkey-prov ${CERTIFICATE} ${PRIVATE_KEY} rc=$? if [[ $rc -eq 77 ]]; then echo "RSA key test skipped." @@ -57,8 +56,7 @@ elif [[ $rc -ne 0 ]]; then exit 1 fi -./check-privkey-prov ${CERTIFICATE_URL} ${PRIVATE_KEY} \ - "${outdir}/engines.cnf" +./check-privkey-prov ${CERTIFICATE_URL} ${PRIVATE_KEY} if [[ $? -ne 0 ]]; then echo "The private key loading couldn't get the public key from the certificate URL." exit 1