ECDSA Verification Without On-Curve Check
Maintainers usually reply within 2 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- c
- Domain
- cryptography, security
Research direction
Start by reading dtls_ecdsa_verify_sig_hash() in crypto.c and ecc_ecdsa_validate() and ec_add() in ecc/ecc.c, focusing on how public-key coordinates are handled. Build and run the PoC in vuln_003_ecdsa_no_on_curve_check/ to reproduce the reported acceptance of a signature under (0, 0). Done means the invalid-point signature is rejected and the relevant verification behavior is covered by a test.
Written by the indexing model from the issue text.
Description
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))returnsu1*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
zwith a small leading byte (ensuring
z < n). - Computes
R = z * Gusing the library's own point multiplication. - Sets
r = R.x(raw) ands = 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
CertificateVerifymessage with the forged signature
(r, 1)wherer = 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_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). - The
verify_ecdsa_keycallback 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)
- Dominant language
- C
- Stars
- 122
- Forks
- 67
- Avg merge
- 6d 8h
- Merged PRs (30d)
- 5
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from eclipse-tinydtls/tinydtls
-
ECDHE_ECDSA ClientKeyExchange rejected as too short when DTLS_PSK_MAX_CLIENT_IDENTITY_LEN > 64Possibly taken A pull request linked to this issue is open or already merged. Openbug good first issue please retest
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
eclipse-tinydtls/tinydtls#298 · 2 comments ·
Maintainers usually reply within 2 days
-
`dtls_get_cookie`: dereference before the length check (currently masked by `dtls_create_cookie`)Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
eclipse-tinydtls/tinydtls#279 · 2 comments · 1 reaction ·
Maintainers usually reply within 2 days
-
MSVC build is broken (undeclared `DTLS_DEBUG_BUF_SIZE`)Possibly taken @mrdeep1 claimed this 9 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 25/100
eclipse-tinydtls/tinydtls#283 · 5 comments ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 57/100
eclipse-tinydtls/tinydtls#278 · 3 comments · 1 reaction ·
Maintainers usually reply within 2 days
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 48/100
eclipse-tinydtls/tinydtls#273 · 2 comments ·
Maintainers usually reply within 2 days
All issues in eclipse-tinydtls/tinydtls
Similar issues
-
docs: AGENTS.md has no rule against running deploy scripts into the owner's game installPossibly taken @thesprockee claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 85/100
EchoTools/nevr-runtime#524 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 74/100
Genymobile/scrcpy#7097 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
-
type: feature
Difficulty 1/5 Under an hour Newbie friendliness 77/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
Maintainers usually reply within 1 day