ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ecdsa.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4#ifndef PROTOCORE_ECDSA_H
5#define PROTOCORE_ECDSA_H
6
7#include "protocore_config.h" // the entry point: protocore_types.h for the widths
8
10
11/**
12 * @file ecdsa.h
13 * @brief NIST P-256 primitives for SSH: ECDSA signatures and ECDH (RFC 5656 / FIPS 186-4).
14 *
15 * Backs three P-256 SSH mechanisms, all sharing the one curve:
16 * - ecdsa-sha2-nistp256 host key + client publickey auth (RFC 5656 §3): the server signs
17 * the KEX exchange hash with its P-256 host key and verifies a client's signature.
18 * - ecdh-sha2-nistp256 key exchange (RFC 5656 §4): the P-256 ECDH shared secret.
19 * ECDSA always hashes the message with SHA-256 (nistp256 pairs with SHA-256, RFC 5656 §6.2.1).
20 *
21 * ═══════════════════════════════════════════════════════════════════════════
22 * THE TWO ARMS
23 * ═══════════════════════════════════════════════════════════════════════════
24 *
25 * One self-contained software P-256 serves both: 256-bit field and scalar arithmetic, the
26 * exception-free complete addition formulas, a constant-time fixed-window scalar multiply,
27 * and RFC 6979 deterministic signing, so the sign path is byte-exact against the RFC 6979
28 * A.2.5 (P-256/SHA-256) known-answer vectors on every target.
29 *
30 * Only the field multiply changes arm. A die whose MPI accelerator carries a single-shot
31 * MODMULT does each 256-bit multiply on it; every other target, and a host build, runs the
32 * software product. The vectors are the same either way, which is what makes the native run
33 * a check on the accelerated one.
34 *
35 * The entries below are one surface over every arm: which one runs the curve math is this
36 * module's and is never visible here.
37 *
38 * ═══════════════════════════════════════════════════════════════════════════
39 * WIRE FORMATS (assembled by the SSH transport/auth layers, not here)
40 * ═══════════════════════════════════════════════════════════════════════════
41 *
42 * Public-key blob (RFC 5656 §3.1):
43 * string("ecdsa-sha2-nistp256") || string("nistp256") || string(Q)
44 * where Q is the uncompressed point 0x04 || X || Y (65 bytes). This module writes Q
45 * through @ref EcdsaNs::pubkey; the layers wrap it.
46 *
47 * Signature blob (RFC 5656 §3.1.2):
48 * string("ecdsa-sha2-nistp256") || string( mpint(r) || mpint(s) )
49 * This module writes the raw r || s (32 + 32 big-endian); the layers mpint-wrap them.
50 *
51 * ECDH shared secret (RFC 5656 §4):
52 * K = the X coordinate of d * Q_peer. @ref EcdsaNs::ecdh writes the raw 32-byte X;
53 * the transport encodes it as an mpint in the exchange hash and the key derivation.
54 *
55 * @c work is PROTOCORE_ECDSA_BORROW secure bytes the CALLER took, at an address it knows. It is not held past the call,
56 * so nothing here aliases it. The caller releases it, and the pool wipes on release; this module neither takes it,
57 * holds it, releases it, nor wipes it. That is what keeps the message hash and the RFC 6979 nonce chain from outliving
58 * the caller.
59 *
60 * @author Douglas Quigg (dstroy0)
61 * @date 2026
62 */
63
64/** @brief P-256 private key (scalar d) length. */
65#define PROTOCORE_ECDSA_P256_PRIV_LEN 32
66
67/** @brief P-256 coordinate length (one of X, Y). */
68#define PROTOCORE_ECDSA_P256_COORD_LEN 32
69
70/** @brief P-256 uncompressed public point length: 0x04 || X || Y. */
71#define PROTOCORE_ECDSA_P256_PUB_LEN 65
72
73/** @brief Raw ECDSA signature length: r || s (32 + 32, big-endian). */
74#define PROTOCORE_ECDSA_P256_SIG_LEN 64
75
76/** @brief Dispatch table. Addressed by offset, so the layout is asserted below. */
77typedef struct
78{
79 proto_bool (*pubkey)(uint8_t *, const uint8_t *, uint8_t *);
80 proto_bool (*sign)(uint8_t *, const uint8_t *, size_t, const uint8_t *, uint8_t *);
81 proto_bool (*verify)(uint8_t *, const uint8_t *, const uint8_t *, size_t, const uint8_t *);
82 proto_bool (*ecdh)(uint8_t *, const uint8_t *, const uint8_t *, uint8_t *);
83} EcdsaNs;
84PROTOCORE_NS_LAYOUT(EcdsaNs, pubkey, sign, verify, ecdh);
85
86/**
87 * @brief Derive Q = d*G and write it uncompressed.
88 * @param work PROTOCORE_ECDSA_BORROW bytes the caller took. Not held past the call.
89 * @param priv PROTOCORE_ECDSA_P256_PRIV_LEN big-endian scalar d, 1 <= d < n
90 * @param pub PROTOCORE_ECDSA_P256_PUB_LEN bytes: 0x04 || X || Y
91 * @return PROTO_TRUE on success.
92 */
93proto_bool protocore_ecdsa_pubkey(uint8_t *work, const uint8_t *priv, uint8_t *pub);
94/**
95 * @brief Hash the message with SHA-256 and write the raw r || s.
96 * @param work PROTOCORE_ECDSA_BORROW bytes the caller took. Not held past the call.
97 * @param msg the message, hashed with SHA-256 here
98 * @param mlen its length
99 * @param priv PROTOCORE_ECDSA_P256_PRIV_LEN big-endian scalar d
100 * @param sig PROTOCORE_ECDSA_P256_SIG_LEN bytes: r || s, 32 + 32 big-endian
101 * @return PROTO_TRUE on success.
102 */
103proto_bool protocore_ecdsa_sign(uint8_t *work, const uint8_t *msg, size_t mlen, const uint8_t *priv, uint8_t *sig);
104/**
105 * @brief Hash the message with SHA-256 and check r || s against the point.
106 * @param work PROTOCORE_ECDSA_BORROW bytes the caller took. Not held past the call.
107 * @param pub PROTOCORE_ECDSA_P256_PUB_LEN uncompressed point, rejected if not on-curve
108 * @param msg the signed message
109 * @param mlen its length
110 * @param sig PROTOCORE_ECDSA_P256_SIG_LEN bytes: r || s, 32 + 32 big-endian
111 * @return PROTO_TRUE on success.
112 */
113proto_bool protocore_ecdsa_verify(uint8_t *work, const uint8_t *pub, const uint8_t *msg, size_t mlen,
114 const uint8_t *sig);
115/**
116 * @brief Write the X coordinate of d * Q_peer.
117 * @param work PROTOCORE_ECDSA_BORROW bytes the caller took. Not held past the call.
118 * @param peer_pub PROTOCORE_ECDSA_P256_PUB_LEN uncompressed peer point 0x04 || X || Y
119 * @param priv PROTOCORE_ECDSA_P256_PRIV_LEN big-endian scalar d, 1 <= d < n
120 * @param shared_x PROTOCORE_ECDSA_P256_COORD_LEN big-endian X coordinate of d * Q_peer
121 * @return PROTO_TRUE on success.
122 */
123proto_bool protocore_ecdsa_ecdh(uint8_t *work, const uint8_t *peer_pub, const uint8_t *priv, uint8_t *shared_x);
124
125/** @brief Module namespace. */
130
132
133#endif // PROTOCORE_ECDSA_H
proto_bool protocore_ecdsa_ecdh(uint8_t *work, const uint8_t *peer_pub, const uint8_t *priv, uint8_t *shared_x)
Write the X coordinate of d * Q_peer.
PROTOCORE_NS EcdsaNs Ecdsa PROTOCORE_UNUSED
Module namespace.
Definition ecdsa.h:126
proto_bool protocore_ecdsa_pubkey(uint8_t *work, const uint8_t *priv, uint8_t *pub)
Derive Q = d*G and write it uncompressed.
proto_bool protocore_ecdsa_sign(uint8_t *work, const uint8_t *msg, size_t mlen, const uint8_t *priv, uint8_t *sig)
Hash the message with SHA-256 and write the raw r || s.
proto_bool protocore_ecdsa_verify(uint8_t *work, const uint8_t *pub, const uint8_t *msg, size_t mlen, const uint8_t *sig)
Hash the message with SHA-256 and check r || s against the point.
#define PROTOCORE_NS_LAYOUT(T,...)
Pin every dispatch slot of a table that is nothing but function pointers.
#define PROTOCORE_NS
Storage for a dispatch table. The const is load bearing.
Dispatch table. Addressed by offset, so the layout is asserted below.
Definition ecdsa.h:78
proto_bool(* pubkey)(uint8_t *, const uint8_t *, uint8_t *)
Definition ecdsa.h:79
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97