Skip to content
ecdsa.com
C

Mbed TLS (TF-PSA-Crypto)

Mbed TLS 4.x, the embedded TLS stack whose cryptography is now the PSA Crypto API: raw r‖s ECDSA signatures, deterministic ECDSA as a separate algorithm, and key policies that fix the algorithm per key.
License
Apache-2.0 OR GPL-2.0-or-later
Checked version
4.1.1 ·
Source
github.com/Mbed-TLS/mbedtls
Documentation
mbed-tls.readthedocs.io/

Compatibility at a glance

ECDSAPost-quantum

ECDSA curves
P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
none
Public key formats
spki-dersec1-uncompressed

Mbed TLS 4 moved its cryptography into TF-PSA-Crypto and the PSA Crypto API (psa_sign_message, psa_verify_message, psa_generate_key), the same API used by Arm's Trusted Firmware. The facts here were checked on the Alpine 3.24.2 package, Mbed TLS 4.1.1 (upstream had reached 4.2.0); features depend on the build configuration, as with every embedded library.

The signature encoding is the one that differs from OpenSSL-derived code: PSA returns the raw r‖s form, 64 bytes on P-256, not DER. Signatures for X.509, CMS or an OpenSSL verifier must be wrapped in DER, and DER signatures from those peers must be unwrapped before psa_verify_message, which otherwise reports PSA_ERROR_INVALID_SIGNATURE — the same status as for a forged signature or a 3-byte blob. Convert with the DER ⇄ raw converter.

Each key carries a policy with one permitted algorithm, so the choice between PSA_ALG_ECDSA (random nonce) and PSA_ALG_DETERMINISTIC_ECDSA (RFC 6979 — the RFC's test vector reproduced) is made when the key is created, as the sample shows. s is not normalized and verification accepts high-S. psa_sign_hash signs a precomputed digest.

This build signed and verified P-256, P-384, P-521, secp256k1 and the Brainpool r1 curves; P-224 was not available, and Ed25519 key generation was refused (PSA_ERROR_NOT_SUPPORTED). Post-quantum support is limited to LMS hash-based signature verification (MBEDTLS_LMS_C); there is no ML-KEM or ML-DSA key type, and the experimental ML-DSA driver from mldsa-native (TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED) is commented out in this build's configuration. Public keys export as uncompressed SEC1 points through PSA and as SPKI DER through the pk layer.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/mbedtls/run.sh ran Mbed TLS (TF-PSA-Crypto) 4.1.1 in the container image alpine:3.24.2 on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    raw

    psa_sign_message returned 64 bytes of raw r‖s on P-256.

    Recorded by the check
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: psa_sign_message returned 64 bytes
  • ECDSA nonce

    rfc6979-opt-in

    PSA_ALG_ECDSA uses a random nonce; PSA_ALG_DETERMINISTIC_ECDSA reproduced RFC 6979 appendix A.2.5.

    Recorded by the check
    fact ecdsa-nonce = rfc6979-opt-in
    evidence ecdsa-nonce: PSA_ALG_ECDSA repeats: no; PSA_ALG_DETERMINISTIC_ECDSA reproduces the RFC 6979 A.2.5 r: yes (status 0)
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
  • High-S when verifying

    accepts-high-s

    A signature with s replaced by n − s verified.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • What the signing call hashes

    hashes-message-or-digest

    psa_sign_message hashes; psa_sign_hash signs a digest, and that signature verified with psa_verify_message.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
  • Malformed ECDSA signature

    returns-false

    A DER signature, a 3-byte blob and a valid signature over other data all returned PSA_ERROR_INVALID_SIGNATURE (-149).

    Recorded by the check
    fact ecdsa-malformed-signature = returns-false
    evidence ecdsa-malformed-signature: psa_verify_message status: DER -149, 3 bytes -149, wrong message -149 (PSA_ERROR_INVALID_SIGNATURE = -149)
  • ECDSA curves

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256, P-384, P-521, secp256k1 and brainpoolP256r1/P384r1/P512r1 signed and verified; P-224 key generation failed in this build.

    Recorded by the check
    fact ecdsa-curves = P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    none

    Ed25519 key generation returned PSA_ERROR_NOT_SUPPORTED (-134).

    Recorded by the check
    fact eddsa = none
    evidence eddsa: psa_generate_key for an Ed25519 key: status -134
  • Post-quantum algorithms

    LMS

    MBEDTLS_LMS_C is enabled (LMS signature verification); no ML-KEM or ML-DSA PSA key type is defined, and the experimental mldsa-native driver is not built.

    Recorded by the check
    fact pqc = LMS
    evidence pqc: MBEDTLS_LMS_C is enabled (LMS hash-based signature verification, <mbedtls/lms.h>)
    evidence pqc: no ML-DSA or ML-KEM PSA key type is defined in these headers
    evidence pqc: psa/crypto_config.h of this build: //#define TF_PSA_CRYPTO_PQCP_MLDSA_ENABLED;//#define TF_PSA_CRYPTO_PQCP_MLDSA_87_ENABLED (commented out = the experimental mldsa-native driver is not built)
  • Public key formats

    spki-dersec1-uncompressed

    psa_export_public_key returns the 65-byte uncompressed SEC1 point; SPKI DER is written and parsed through the pk layer.

    Recorded by the check
    fact public-key-formats = spki-der, sec1-uncompressed
    evidence public-key-formats: psa_export_public_key returns 65 bytes starting 0x04 (an uncompressed SEC1 point)

Usage

Create a P-256 key whose policy permits deterministic ECDSA, sign a message and verify the 64-byte raw signature.
#include <psa/crypto.h>
#include <stdio.h>

int main(void) {
    if (psa_crypto_init() != PSA_SUCCESS) return 1;
    /* The algorithm is part of the key policy: here deterministic ECDSA (RFC 6979) with SHA-256. */
    psa_algorithm_t alg = PSA_ALG_DETERMINISTIC_ECDSA(PSA_ALG_SHA_256);
    psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
    psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
    psa_set_key_bits(&attr, 256);
    psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE);
    psa_set_key_algorithm(&attr, alg);
    psa_key_id_t key;
    if (psa_generate_key(&attr, &key) != PSA_SUCCESS) return 1;

    const uint8_t msg[] = "order #1042 approved";
    uint8_t sig[PSA_SIGNATURE_MAX_SIZE];
    size_t sig_len;
    psa_sign_message(key, alg, msg, sizeof msg - 1, sig, sizeof sig, &sig_len);

    /* PSA signatures are raw r||s (64 bytes on P-256): wrap them in DER for X.509 or OpenSSL peers. */
    psa_status_t status = psa_verify_message(key, alg, msg, sizeof msg - 1, sig, sig_len);
    printf("signature bytes: %zu, verified: %d\n", sig_len, status == PSA_SUCCESS);
    psa_destroy_key(key);
    return 0;
}

The snippet harness runs every sample verbatim in the container image alpine:3.24.2, and the run fails unless the output matches.

Setup
apk add --no-cache -q build-base mbedtls-dev=4.1.1-r0 >/dev/null
Command
cc -o usage usage.c -lmbedcrypto -ltfpsacrypto && ./usage
Output must match (regular expression)
signature bytes: 64, verified: 1

Libraries