DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_ecdsa.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ssh_ecdsa.h
6 * @brief NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
7 *
8 * Backs three P-256 SSH mechanisms, all sharing the one curve:
9 * - ecdsa-sha2-nistp256 host key + client publickey auth (RFC 5656 §3): the server signs
10 * the KEX exchange hash with its P-256 host key and verifies a client's signature.
11 * - ecdh-sha2-nistp256 key exchange (RFC 5656 §4): the P-256 ECDH shared secret.
12 * ECDSA always hashes the message with SHA-256 (nistp256 pairs with SHA-256, RFC 5656 §6.2.1).
13 *
14 * ═══════════════════════════════════════════════════════════════════════════
15 * ARDUINO VS NATIVE
16 * ═══════════════════════════════════════════════════════════════════════════
17 *
18 * Arduino: mbedTLS (mbedtls_ecdsa_*, mbedtls_ecp_*) - hardware-accelerated big-integer
19 * math on the ESP32 and side-channel-hardened. This is the production path. Signing
20 * uses the ESP32 hardware RNG (randomized ECDSA, RFC 6979 not required for validity).
21 *
22 * Native: self-contained software P-256 - 256-bit field/scalar arithmetic (bit-serial
23 * reduction mod p and mod n), Jacobian point arithmetic, and RFC 6979 deterministic
24 * signing so the sign path is byte-exact against the RFC 6979 A.2.5 (P-256/SHA-256)
25 * known-answer vectors. Test-only, like the native RSA path; not compiled into firmware.
26 *
27 * ═══════════════════════════════════════════════════════════════════════════
28 * WIRE FORMATS (assembled by the SSH transport/auth layers, not here)
29 * ═══════════════════════════════════════════════════════════════════════════
30 *
31 * Public-key blob (RFC 5656 §3.1):
32 * string("ecdsa-sha2-nistp256") || string("nistp256") || string(Q)
33 * where Q is the uncompressed point 0x04 || X || Y (65 bytes). This module exposes Q
34 * as @ref ssh_ecdsa_p256_pubkey; the layers wrap it.
35 *
36 * Signature blob (RFC 5656 §3.1.2):
37 * string("ecdsa-sha2-nistp256") || string( mpint(r) || mpint(s) )
38 * This module exposes the raw r || s (32 + 32 big-endian); the layers mpint-wrap them.
39 *
40 * ECDH shared secret (RFC 5656 §4):
41 * K = the X coordinate of d * Q_peer. @ref ssh_ecdsa_p256_ecdh returns the raw 32-byte X;
42 * the transport encodes it as an mpint in the exchange hash and the key derivation.
43 *
44 * @author Douglas Quigg (dstroy0)
45 * @date 2026
46 */
47
48#ifndef DETERMINISTICESPASYNCWEBSERVER_SSH_ECDSA_H
49#define DETERMINISTICESPASYNCWEBSERVER_SSH_ECDSA_H
50
51#include <stddef.h>
52#include <stdint.h>
53
54/** @brief P-256 private key (scalar d) length. */
55static constexpr size_t SSH_ECDSA_P256_PRIV_LEN = 32;
56/** @brief P-256 coordinate length (one of X, Y). */
57static constexpr size_t SSH_ECDSA_P256_COORD_LEN = 32;
58/** @brief P-256 uncompressed public point length: 0x04 || X || Y. */
59static constexpr size_t SSH_ECDSA_P256_PUB_LEN = 65;
60/** @brief Raw ECDSA signature length: r || s (32 + 32, big-endian). */
61static constexpr size_t SSH_ECDSA_P256_SIG_LEN = 64;
62
63/**
64 * @brief Derive the uncompressed public point Q = d*G from a P-256 private scalar.
65 *
66 * @param[out] pub 65-byte uncompressed point 0x04 || X || Y.
67 * @param[in] priv 32-byte big-endian private scalar d (must satisfy 1 <= d < n).
68 * @return true on success, false if @p priv is 0 or >= the group order n.
69 */
70bool ssh_ecdsa_p256_pubkey(uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN]);
71
72/**
73 * @brief Sign @p mlen bytes of @p msg with a P-256 private key (ECDSA, SHA-256).
74 *
75 * The message is hashed with SHA-256 internally. Native builds sign deterministically
76 * (RFC 6979); Arduino builds sign with the hardware RNG. Both produce a valid signature.
77 *
78 * @param[out] sig 64-byte raw signature r || s (big-endian, 32 + 32).
79 * @param[in] msg Message to sign (typically the KEX exchange hash H).
80 * @param[in] mlen Length of @p msg.
81 * @param[in] priv 32-byte big-endian private scalar d.
82 * @return true on success, false on invalid key or internal failure.
83 */
84bool ssh_ecdsa_p256_sign(uint8_t sig[SSH_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen,
85 const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN]);
86
87/**
88 * @brief Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
89 *
90 * @param[in] pub 65-byte uncompressed point 0x04 || X || Y (rejected if not on-curve).
91 * @param[in] msg Signed message.
92 * @param[in] mlen Length of @p msg.
93 * @param[in] sig 64-byte raw signature r || s (big-endian, 32 + 32).
94 * @return true if the signature is valid, false otherwise.
95 */
96bool ssh_ecdsa_p256_verify(const uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen,
97 const uint8_t sig[SSH_ECDSA_P256_SIG_LEN]);
98
99/**
100 * @brief P-256 ECDH: the shared-secret X coordinate of d * Q_peer (RFC 5656 §4 / RFC 5903).
101 *
102 * Backs the ecdh-sha2-nistp256 SSH key exchange. Validates that @p peer_pub is a valid
103 * on-curve point and that the product is not the identity; the returned 32-byte big-endian
104 * X coordinate is the shared field element the transport encodes as an mpint.
105 *
106 * @param[out] shared_x 32-byte big-endian X coordinate of d * Q_peer.
107 * @param[in] peer_pub 65-byte uncompressed peer point 0x04 || X || Y.
108 * @param[in] priv 32-byte big-endian private scalar d (1 <= d < n).
109 * @return true on success, false on an invalid peer point / scalar or an identity result.
110 */
111bool ssh_ecdsa_p256_ecdh(uint8_t shared_x[SSH_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[SSH_ECDSA_P256_PUB_LEN],
112 const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN]);
113
114#endif // DETERMINISTICESPASYNCWEBSERVER_SSH_ECDSA_H
bool ssh_ecdsa_p256_verify(const uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t *msg, size_t mlen, const uint8_t sig[SSH_ECDSA_P256_SIG_LEN])
Verify a P-256 ECDSA signature (SHA-256) against an uncompressed public point.
bool ssh_ecdsa_p256_ecdh(uint8_t shared_x[SSH_ECDSA_P256_COORD_LEN], const uint8_t peer_pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
P-256 ECDH: the shared-secret X coordinate of d * Q_peer (RFC 5656 §4 / RFC 5903).
bool ssh_ecdsa_p256_sign(uint8_t sig[SSH_ECDSA_P256_SIG_LEN], const uint8_t *msg, size_t mlen, const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
Sign mlen bytes of msg with a P-256 private key (ECDSA, SHA-256).
bool ssh_ecdsa_p256_pubkey(uint8_t pub[SSH_ECDSA_P256_PUB_LEN], const uint8_t priv[SSH_ECDSA_P256_PRIV_LEN])
Derive the uncompressed public point Q = d*G from a P-256 private scalar.
Definition ssh_ecdsa.cpp:79