1. Report Metadata
| Field |
Value |
| Project |
Eclipse tinyDTLS |
| Title |
ECDSA signature verification accepts forged signatures under an invalid (identity-point) public key |
| Affected component |
crypto.c, ecc/ecc.c |
| Affected function |
dtls_ecdsa_verify_sig_hash() → ecc_ecdsa_validate() → ec_add() |
| Affected role |
DTLS client verifying server certificate; DTLS server verifying client certificate (mutual auth) |
| Tested version |
v0.9-rc1-214-g6f4f604 (commit 6f4f604, main) |
| Suggested CWE |
CWE-347 (Improper Verification of Cryptographic Signature), CWE-345 (Insufficient Verification of Data Authenticity) |
| Impact class |
Authentication bypass / signature forgery (bypassable — gated by application callback) |
2. Executive Summary
Eclipse tinyDTLS verifies ECDSA signatures (used in CertificateVerify
and ServerKeyExchange messages) by calling ecc_ecdsa_validate(),
which computes u_1 * G + u_2 * Q_A and compares the result's x-coordinate
to the signature value r. The implementation never checks that the
claimed public key Q_A is a valid point on the P-256 curve, nor that
it is not the point at infinity.
The library's elliptic-curve addition function ec_add() treats the
coordinate (0, 0) as the additive identity (point at infinity). This
is an implementation choice, not a mathematical property of P-256 — on
the real curve, (0, 0) is not even a valid point.
An attacker can exploit this by presenting a certificate or
ServerKeyExchange message whose public key is Q_A = (0, 0). When
the verifier computes u_2 * Q_A, the result is (0, 0) (identity),
and ec_add(u_1 * G, (0, 0)) returns u_1 * G unchanged. The
verification then reduces to checking x(u_1 * G) == r, which the
attacker can satisfy by choosing s = 1 and computing r = x(z * G)
where z is the transcript hash. The attacker does not need the
private key corresponding to the forged public key.
The forgery is gated by the application's verify_ecdsa_key callback:
tinydtls itself never validates the key, so any application that does
not pin the expected peer public key (e.g. trust-on-first-use,
accept-any-self-signed-cert, or missing callback) is vulnerable. When
the callback is absent or permissive, the attacker can impersonate any
server (or client, in mutual-auth mode) and complete the DTLS
handshake, deriving shared session keys without possessing any valid
credential.
3. Vulnerability Overview
The verifier converts the attacker-supplied public key and calls the
ECC validation routine with no on-curve check:
@tinydtls/crypto.c:521-537
int
dtls_ecdsa_verify_sig_hash(const unsigned char *pub_key_x,
const unsigned char *pub_key_y, size_t key_size,
const unsigned char *sign_hash, size_t sign_hash_size,
unsigned char *result_r, unsigned char *result_s) {
uint32_t pub_x[8];
uint32_t pub_y[8];
...
dtls_ec_key_to_uint32(pub_key_x, key_size, pub_x);
dtls_ec_key_to_uint32(pub_key_y, key_size, pub_y);
...
return ecc_ecdsa_validate(pub_x, pub_y, hash, point_r, point_s);
}
The validation routine computes u_2 * Q_A and adds it to u_1 * G
without checking Q_A:
@tinydtls/ecc/ecc.c:612-651
int ecc_ecdsa_validate(const uint32_t *x, const uint32_t *y, const uint32_t *e, const uint32_t *r, const uint32_t *s)
{
...
if (isZero(r) || isZero(s))
return -1;
// 3. Calculate w = s^{-1} \pmod{n}
fieldInv(s, ecc_order_m, ecc_order_r, w);
// 4. Calculate u_1 = zw \pmod{n}
fieldMult(e, w, tmp, arrayLength);
fieldModO(tmp, u1, 16);
// 4. Calculate u_2 = rw \pmod{n}
fieldMult(r, w, tmp, arrayLength);
fieldModO(tmp, u2, 16);
// 5. Calculate the curve point (x_1, y_1) = u_1 * G + u_2 * Q_A.
// tmp1 = u_1 * G
ecc_ec_mult(ecc_g_point_x, ecc_g_point_y, u1, tmp1_x, tmp1_y);
// tmp2 = u_2 * Q_A
ecc_ec_mult(x, y, u2, tmp2_x, tmp2_y);
// tmp3 = tmp1 + tmp2
ec_add(tmp1_x, tmp1_y, tmp2_x, tmp2_y, tmp3_x, tmp3_y);
return isSame(tmp3_x, r, arrayLength) ? 0 : -1;
}
The ec_add function treats (0, 0) as the identity:
@tinydtls/ecc/ecc.c:466-480
static void ec_add(const uint32_t *px, const uint32_t *py, const uint32_t *qx, const uint32_t *qy, uint32_t *Sx, uint32_t *Sy){
...
if(isZero(px) && isZero(py)){
copy(qx, Sx,arrayLength);
copy(qy, Sy,arrayLength);
return;
} else if(isZero(qx) && isZero(qy)) {
copy(px, Sx,arrayLength);
copy(py, Sy,arrayLength);
return;
}
...
}
There is no ecc_is_on_curve() function anywhere in the codebase
(verified by grep). The only key-validation function,
ecc_is_valid_key() at ecc.c:653, checks that a private scalar
is less than n — it does not validate public points.
4. Technical Root Cause
Standard ECDSA verification
Given signature (r, s), hash z, and public key Q_A (a valid
curve point), verification computes:
w = s^{-1} mod n
u1 = z * w mod n
u2 = r * w mod n
R = u1 * G + u2 * Q_A
accept if x(R) mod n == r
The critical precondition is that Q_A is a valid, non-identity point
on the correct curve. If Q_A is malformed, the verification is
meaningless.
tinydtls's missing check
dtls_ecdsa_verify_sig_hash() receives pub_key_x and pub_key_y
from the peer's certificate or ServerKeyExchange message and passes
them directly to ecc_ecdsa_validate() after byte/word conversion.
At no point does it verify:
- That
(pub_key_x, pub_key_y) satisfies the P-256 curve equation
y² = x³ - 3x + b (mod p).
- That the point is not the point at infinity.
- That the point is in the correct subgroup (has order
n).
The identity-point attack
ec_add() uses (0, 0) as its representation of the point at
infinity (the additive identity). This is a valid implementation
choice for affine coordinates, but it means that any caller can inject
the identity point by supplying (0, 0) as a public key.
When Q_A = (0, 0):
ecc_ec_mult((0,0), ..., u2) returns (0, 0) — multiplying the
identity by any scalar yields the identity.
ec_add(u1*G, (0,0)) returns u1*G — adding the identity to any
point leaves it unchanged.
- The verification check becomes
x(u1*G) == r.
The attacker controls z (the transcript hash) and chooses s = 1:
w = 1^{-1} mod n = 1
u1 = z * 1 mod n = z (when z < n)
u2 = r * 1 mod n = r
R = z * G + r * (0,0) = z * G
The attacker computes R = z * G using the public generator, sets
r = R.x (the raw x-coordinate), and s = 1. The signature (r, 1)
verifies under the forged key (0, 0).
Why the PoC chooses z < n
ecc_ec_mult takes a 256-bit scalar but does not reduce it mod n
before multiplication. The verifier, however, computes
u1 = z * w mod n. With s = 1, u1 = z mod n. If z >= n, then
z mod n != z, and u1 * G != z * G. By choosing a hash z < n
(any 32-byte value with a leading byte below 0xFF is guaranteed to
be below the P-256 order), the attacker ensures u1 = z and the
forgery works.
In a real DTLS handshake, z is the SHA-256 of the transcript
(truncated to 256 bits). The attacker cannot freely choose z, but
the forgery works for any z < n, which holds with overwhelming
probability (the probability that a random 256-bit hash is >= n is
approximately 2^{-128}).
Gating condition
The attack requires the application's verify_ecdsa_key callback to
accept the forged key (0, 0). tinydtls itself never checks the key,
so the gate is entirely application-defined:
- If the callback pins the expected peer key (comparing against a
known-good public key), the forged key is rejected and the attack
fails.
- If the callback is missing, returns 0 unconditionally, or performs
only a structural check (e.g. "is it 32 bytes?"), the attack
succeeds.
This makes the vulnerability bypassable — its exploitability
depends on the integrator's code. However, the library's failure to
perform on-curve validation is a defense-in-depth gap that should be
fixed at the library level, not delegated to every application.
5. Proof of Concept
The PoC (poc.c) #includes dtls.c and links ecc.c with
TEST_INCLUDE to access the internal ecc_ec_mult helper. It:
- Constructs the forged public key
Q_A = (0, 0).
- Chooses a 32-byte hash
z with a small leading byte (ensuring
z < n).
- Computes
R = z * G using the library's own point multiplication.
- Sets
r = R.x (raw) and s = 1.
- Calls
dtls_ecdsa_verify_sig_hash() with the forged key and
signature.
The core forgery logic:
/* Forged public key = identity point (0,0) */
memset(forged_pub_x, 0, 32);
memset(forged_pub_y, 0, 32);
/* Hash z < n (small leading byte) */
hash_be[0] = 0x01;
for (int i = 1; i < 32; i++) hash_be[i] = 0x42;
/* R = z * G, r = R.x, s = 1 */
ecc_ec_mult(ecc_g_point_x, ecc_g_point_y, z, Rx, Ry);
memcpy(r, Rx, 32);
s[0] = 1;
/* Verify under forged key */
int rc = dtls_ecdsa_verify_sig_hash(forged_pub_x, forged_pub_y, 32,
hash_be, 32, r_be, s_be);
/* rc == 0 => signature accepted */
6. Build & Run
Working directory:
tinydtls/vuln_003_ecdsa_no_on_curve_check/
cmake -B build -S .
cmake --build build -j$(nproc)
./build/vuln_poc_ecdsa_no_on_curve_check
Expected output:
tinydtls PoC -- Finding 3: ECDSA no on-curve check (forgery)
=============================================================
Forged public key Q_A = (0, 0) [identity point]
Hash z (transcript SHA-256) : 0142424242424242424242424242424242424242424242424242424242424242
(z < n, so u1 = z when s = 1)
Chosen s = 1 (w = 1, u1 = z, u2 = r)
Computed R = z * G, r = R.x:
r = 0be920db0d543be3526d20ab5e8f3497194dc56d8db86172149ebcc3b9059173
s = 0000000000000000000000000000000000000000000000000000000000000001
dtls_ecdsa_verify_sig_hash returned: 0
(0 = signature valid, -1 = invalid)
[!] Vulnerability reproduced: ECDSA signature forgery
accepted under forged public key (0,0).
The library performs NO on-curve / infinity check on
the claimed public key (crypto.c:531-537, ecc.c:612).
CWE-347 (Improper Verification of Cryptographic Signature)
CWE-345 (Insufficient Verification of Data Authenticity)
Done.
Exit code: 0 (forgery accepted).
The verifier returns 0 (signature valid) for a signature produced
without knowledge of any private key, under a public key that is
not a valid P-256 point.
7. Impact
Attacker position: A remote, unauthenticated network peer who can
inject a forged certificate or ServerKeyExchange message into the
DTLS handshake. On the client side, this is a malicious server
impersonating a legitimate one. On the server side (mutual
authentication), this is a malicious client.
Authentication bypass: When the application's verify_ecdsa_key
callback does not pin the expected peer public key, the attacker can:
- Present a certificate containing the public key
(0, 0).
- Send a
CertificateVerify message with the forged signature
(r, 1) where r = x(z * G).
- The verifier accepts the signature, completing the handshake.
- The attacker derives the same premaster secret as the legitimate
peer would, using the ECDH key exchange parameters the attacker
itself supplied.
This breaks the authentication guarantee of DTLS: the attacker
establishes an encrypted channel without possessing any valid
credential for the identity being claimed.
Gating caveat: The attack is bypassable — it only succeeds
when the integrator's verify_ecdsa_key callback accepts the forged
key. Applications that pin peer keys (comparing the received public
key against a known-good value) are not vulnerable. However, the
library should not rely on every application to perform on-curve
validation; this is a standard defensive check that belongs in the
library.
CWE-347 (Improper Verification of Cryptographic Signature) /
CWE-345 (Insufficient Verification of Data Authenticity).
Severity: Medium. Bypassable via application callback, but when
the callback is permissive or absent, the impact is full
authentication bypass.
8. Remediation
Add an on-curve check to ecc_ecdsa_validate() (or to
dtls_ecdsa_verify_sig_hash() before calling it). The check should
verify that Q_A = (pub_x, pub_y) satisfies the P-256 curve equation
and is not the point at infinity.
Suggested implementation — add a new function to ecc.c:
/* Returns 1 if (x, y) is a valid non-identity point on P-256, 0 otherwise. */
int ecc_is_on_curve(const uint32_t *x, const uint32_t *y) {
uint32_t lhs[16], rhs[16];
uint32_t tmp[16];
/* Reject the identity (0,0) used by ec_add */
if (isZero(x) && isZero(y))
return 0;
/* Check 0 <= x < p and 0 <= y < p */
if (isGreater(x, ecc_prime_m, arrayLength) >= 0)
return 0;
if (isGreater(y, ecc_prime_m, arrayLength) >= 0)
return 0;
/* lhs = y^2 mod p */
fieldMult(y, y, lhs, arrayLength);
fieldModP(lhs, lhs);
/* rhs = x^3 - 3x + b mod p */
fieldMult(x, x, tmp, arrayLength);
fieldModP(tmp, tmp);
fieldMult(tmp, x, rhs, arrayLength);
fieldModP(rhs, rhs);
/* rhs = x^3 - 3x + b; subtract 3x */
uint32_t three[8] = {3,0,0,0,0,0,0,0};
fieldSub(rhs, three, ecc_prime_m, rhs);
/* add b (P-256 b constant) */
fieldAdd(rhs, ecc_b, ecc_prime_r, rhs);
fieldModP(rhs, rhs);
return isSame(lhs, rhs, arrayLength) ? 1 : 0;
}
Then call it at the start of ecc_ecdsa_validate():
int ecc_ecdsa_validate(const uint32_t *x, const uint32_t *y, ...) {
if (!ecc_is_on_curve(x, y))
return -1;
...
}
The P-256 b constant must be defined alongside ecc_prime_m:
/* P-256 curve parameter b */
static const uint32_t ecc_b[8] = {
0x3cecd3ad, 0x4d61a0d3, 0x1d8c2f06, 0x27a2c5a7,
0x9359d6b3, 0xb3970f85, 0x8689e9a4, 0x5ac635d8
};
Additional hardening:
- Also check that
Q_A has order n (i.e., n * Q_A == identity),
to prevent small-subgroup attacks. This is more expensive but
important for ECDH key exchange (ecc_ecdh at crypto.c:456).
- The
verify_ecdsa_key callback should be documented as a
defense-in-depth check, not the primary on-curve validation.
9. References
- CWE-347: Improper Verification of Cryptographic Signature — https://cwe.mitre.org/data/definitions/347.html
- CWE-345: Insufficient Verification of Data Authenticity — https://cwe.mitre.org/data/definitions/345.html
- SEC 1: Elliptic Curve Cryptography, section 4.1 (ECDSA Signature
Verification), step 4: "Verify that Q_A is a valid public key."
- RFC 4492 (TLS ECC), section 5.4: ServerKeyExchange signature
verification.
- RFC 8422 (TLS ECC update), section 5.4.
- NIST FIPS 186-4, section 6.4.2: ECDSA signature verification
precondition: "Q shall be a valid public key."
- tinyDTLS source:
https://github.com/eclipse/tinydtls
- Affected commit:
6f4f604 (v0.9-rc1-214-g6f4f604)
vuln_003_ecdsa_no_on_curve_check.zip
1. Report Metadata
crypto.c,ecc/ecc.cdtls_ecdsa_verify_sig_hash()→ecc_ecdsa_validate()→ec_add()v0.9-rc1-214-g6f4f604(commit6f4f604,main)2. Executive Summary
Eclipse tinyDTLS verifies ECDSA signatures (used in
CertificateVerifyand
ServerKeyExchangemessages) by callingecc_ecdsa_validate(),which computes
u_1 * G + u_2 * Q_Aand compares the result's x-coordinateto the signature value
r. The implementation never checks that theclaimed public key
Q_Ais a valid point on the P-256 curve, nor thatit is not the point at infinity.
The library's elliptic-curve addition function
ec_add()treats thecoordinate
(0, 0)as the additive identity (point at infinity). Thisis an implementation choice, not a mathematical property of P-256 — on
the real curve,
(0, 0)is not even a valid point.An attacker can exploit this by presenting a certificate or
ServerKeyExchangemessage whose public key isQ_A = (0, 0). Whenthe verifier computes
u_2 * Q_A, the result is(0, 0)(identity),and
ec_add(u_1 * G, (0, 0))returnsu_1 * Gunchanged. Theverification then reduces to checking
x(u_1 * G) == r, which theattacker can satisfy by choosing
s = 1and computingr = x(z * G)where
zis the transcript hash. The attacker does not need theprivate key corresponding to the forged public key.
The forgery is gated by the application's
verify_ecdsa_keycallback:tinydtls itself never validates the key, so any application that does
not pin the expected peer public key (e.g. trust-on-first-use,
accept-any-self-signed-cert, or missing callback) is vulnerable. When
the callback is absent or permissive, the attacker can impersonate any
server (or client, in mutual-auth mode) and complete the DTLS
handshake, deriving shared session keys without possessing any valid
credential.
3. Vulnerability Overview
The verifier converts the attacker-supplied public key and calls the
ECC validation routine with no on-curve check:
@tinydtls/crypto.c:521-537
The validation routine computes
u_2 * Q_Aand adds it tou_1 * Gwithout checking
Q_A:@tinydtls/ecc/ecc.c:612-651
The
ec_addfunction treats(0, 0)as the identity:@tinydtls/ecc/ecc.c:466-480
There is no
ecc_is_on_curve()function anywhere in the codebase(verified by grep). The only key-validation function,
ecc_is_valid_key()atecc.c:653, checks that a private scalaris less than
n— it does not validate public points.4. Technical Root Cause
Standard ECDSA verification
Given signature
(r, s), hashz, and public keyQ_A(a validcurve point), verification computes:
The critical precondition is that
Q_Ais a valid, non-identity pointon the correct curve. If
Q_Ais malformed, the verification ismeaningless.
tinydtls's missing check
dtls_ecdsa_verify_sig_hash()receivespub_key_xandpub_key_yfrom the peer's certificate or
ServerKeyExchangemessage and passesthem directly to
ecc_ecdsa_validate()after byte/word conversion.At no point does it verify:
(pub_key_x, pub_key_y)satisfies the P-256 curve equationy² = x³ - 3x + b (mod p).n).The identity-point attack
ec_add()uses(0, 0)as its representation of the point atinfinity (the additive identity). This is a valid implementation
choice for affine coordinates, but it means that any caller can inject
the identity point by supplying
(0, 0)as a public key.When
Q_A = (0, 0):ecc_ec_mult((0,0), ..., u2)returns(0, 0)— multiplying theidentity by any scalar yields the identity.
ec_add(u1*G, (0,0))returnsu1*G— adding the identity to anypoint leaves it unchanged.
x(u1*G) == r.The attacker controls
z(the transcript hash) and choosess = 1:The attacker computes
R = z * Gusing the public generator, setsr = R.x(the raw x-coordinate), ands = 1. The signature(r, 1)verifies under the forged key
(0, 0).Why the PoC chooses
z < necc_ec_multtakes a 256-bit scalar but does not reduce it modnbefore multiplication. The verifier, however, computes
u1 = z * w mod n. Withs = 1,u1 = z mod n. Ifz >= n, thenz mod n != z, andu1 * G != z * G. By choosing a hashz < n(any 32-byte value with a leading byte below
0xFFis guaranteed tobe below the P-256 order), the attacker ensures
u1 = zand theforgery works.
In a real DTLS handshake,
zis the SHA-256 of the transcript(truncated to 256 bits). The attacker cannot freely choose
z, butthe forgery works for any
z < n, which holds with overwhelmingprobability (the probability that a random 256-bit hash is
>= nisapproximately
2^{-128}).Gating condition
The attack requires the application's
verify_ecdsa_keycallback toaccept the forged key
(0, 0). tinydtls itself never checks the key,so the gate is entirely application-defined:
known-good public key), the forged key is rejected and the attack
fails.
only a structural check (e.g. "is it 32 bytes?"), the attack
succeeds.
This makes the vulnerability bypassable — its exploitability
depends on the integrator's code. However, the library's failure to
perform on-curve validation is a defense-in-depth gap that should be
fixed at the library level, not delegated to every application.
5. Proof of Concept
The PoC (
poc.c)#includesdtls.cand linksecc.cwithTEST_INCLUDEto access the internalecc_ec_multhelper. It:Q_A = (0, 0).zwith a small leading byte (ensuringz < n).R = z * Gusing the library's own point multiplication.r = R.x(raw) ands = 1.dtls_ecdsa_verify_sig_hash()with the forged key andsignature.
The core forgery logic:
6. Build & Run
Working directory:
tinydtls/vuln_003_ecdsa_no_on_curve_check/Expected output:
Exit code: 0 (forgery accepted).
The verifier returns
0(signature valid) for a signature producedwithout knowledge of any private key, under a public key that is
not a valid P-256 point.
7. Impact
Attacker position: A remote, unauthenticated network peer who can
inject a forged certificate or
ServerKeyExchangemessage into theDTLS handshake. On the client side, this is a malicious server
impersonating a legitimate one. On the server side (mutual
authentication), this is a malicious client.
Authentication bypass: When the application's
verify_ecdsa_keycallback does not pin the expected peer public key, the attacker can:
(0, 0).CertificateVerifymessage with the forged signature(r, 1)wherer = x(z * G).peer would, using the ECDH key exchange parameters the attacker
itself supplied.
This breaks the authentication guarantee of DTLS: the attacker
establishes an encrypted channel without possessing any valid
credential for the identity being claimed.
Gating caveat: The attack is bypassable — it only succeeds
when the integrator's
verify_ecdsa_keycallback accepts the forgedkey. Applications that pin peer keys (comparing the received public
key against a known-good value) are not vulnerable. However, the
library should not rely on every application to perform on-curve
validation; this is a standard defensive check that belongs in the
library.
CWE-347 (Improper Verification of Cryptographic Signature) /
CWE-345 (Insufficient Verification of Data Authenticity).
Severity: Medium. Bypassable via application callback, but when
the callback is permissive or absent, the impact is full
authentication bypass.
8. Remediation
Add an on-curve check to
ecc_ecdsa_validate()(or todtls_ecdsa_verify_sig_hash()before calling it). The check shouldverify that
Q_A = (pub_x, pub_y)satisfies the P-256 curve equationand is not the point at infinity.
Suggested implementation — add a new function to
ecc.c:Then call it at the start of
ecc_ecdsa_validate():The P-256
bconstant must be defined alongsideecc_prime_m:Additional hardening:
Q_Ahas ordern(i.e.,n * Q_A == identity),to prevent small-subgroup attacks. This is more expensive but
important for ECDH key exchange (
ecc_ecdhatcrypto.c:456).verify_ecdsa_keycallback should be documented as adefense-in-depth check, not the primary on-curve validation.
9. References
Verification), step 4: "Verify that Q_A is a valid public key."
verification.
precondition: "Q shall be a valid public key."
https://github.com/eclipse/tinydtls6f4f604(v0.9-rc1-214-g6f4f604)vuln_003_ecdsa_no_on_curve_check.zip