nx_p384_ecdh.nx source
↩ module page · 120 lines · 5111 B
1// nx_p384_ecdh.nx -- ECDH key exchange on P-384 (secp384r1) for the TLS 1.3 key_share path.
2//
3// Mirrors nx_p256_ecdh.nx 1:1 on the 12-limb / 384-bit stack. The P-384 field/point/scalar_mul shipped for
4// ECDSA-P384 cert VERIFICATION (real chains use P-384 intermediate/root keys); this module composes the SAME
5// primitives into the ECDHE role so the ClientHello can carry a secp384r1 KeyShareEntry next to x25519 +
6// secp256r1. FIPS-strict gov servers (api.bls.gov / api.fiscaldata.treasury.gov / waterservices.usgs.gov)
7// that require P-384 ECDHE + AES-256-GCM-SHA384 then complete WITHOUT a HelloRetryRequest -- they pick our
8// P-384 share. No new curve math: only the ECDH wrapper.
9//
10// Public API (mirrors p256_ecdh):
11// p384_ecdh_derive_priv(seed32, out_priv48) domain-separated ephemeral scalar (SHA-384), reduced mod n.
12// p384_ecdh_pub(priv48, out_pub97) out = 0x04 || X(48) || Y(48) (RFC 8446 secp384r1 wire form).
13// p384_ecdh_shared(priv48, peer_pub97, len, out_shared48) validates peer point, writes x(priv*peer) (RFC 8446 7.4.2).
14//
15// Variable-time scalar_mul: the scalar is an EPHEMERAL per-session secret (one-handshake lifetime), the same
16// accepted tradeoff as p256_ecdh; constant-time ladder is the named follow-up.
17// license_tier: INDEPENDENT_REDERIVE
18// genealogy_id: international-research-sources/ietf/rfc_5903 + ietf/rfc_8446 + nist/fips_186_5
19import "nx_syscalls.nx"
20import "nx_sha512.nx" // sha384_digest -- 48-byte domain-separated ephemeral scalar seed
21import "nx_u384.nx"
22import "nx_p384_field.nx"
23import "nx_p384_point.nx"
24import "nx_p384_point_add.nx"
25import "nx_p384_scalar_mul.nx"
26import "nx_p384_modn.nx"
27
28const NX_P384_ECDH_OK: i64 = 1
29const NX_P384_ECDH_BAD_PRIV: i64 = 2 // scalar zero or >= n
30const NX_P384_ECDH_BAD_POINT: i64 = 3 // format/on-curve/infinity reject
31const NX_P384_ECDH_VERDICT_N: i64 = 4
32
33func nx_p384_ecdh_verdict_is_valid(v: i64) -> i64 {
34 if v < NX_P384_ECDH_OK { return 0 }
35 if v >= NX_P384_ECDH_VERDICT_N { return 0 }
36 return 1
37}
38
39// Load priv48 (big-endian) into 12 limbs and range-check 0 < k < n.
40func _p384_ecdh_load_scalar(priv48: *u8, out_k: *i64) -> i64 {
41 u384_load_be(out_k, priv48)
42 if u384_is_zero(out_k) == 1 { return NX_P384_ECDH_BAD_PRIV }
43 let n: *i64 = u384_alloc()
44 p384_modn_load_n(n)
45 if u384_cmp(out_k, n) >= 0 { return NX_P384_ECDH_BAD_PRIV }
46 return NX_P384_ECDH_OK
47}
48
49// Ephemeral P-384 private key from 32 seed bytes via domain-separated SHA-384 (48-byte digest), conditionally
50// reduced mod n (one subtract: n > 2^383 so any 384-bit digest is < 2n), forced nonzero.
51func p384_ecdh_derive_priv(seed32: *u8, out_priv48: *u8) -> i64 {
52 let tag: *u8 = "NISHI-TLS13-P384-ECDH-V1\x00" as *u8
53 let buf: *u8 = sys_mmap(64)
54 var i: i64 = 0
55 while i < 24 { buf[i] = tag[i]; i = i + 1 }
56 i = 0
57 while i < 32 { buf[24 + i] = seed32[i]; i = i + 1 }
58 let digest: *u8 = sys_mmap(48)
59 sha384_digest(buf, 56, digest)
60
61 let k: *i64 = u384_alloc()
62 u384_load_be(k, digest)
63 let n: *i64 = u384_alloc()
64 p384_modn_load_n(n)
65 if u384_cmp(k, n) >= 0 { u384_sub_with_borrow(k, k, n) }
66 if u384_is_zero(k) == 1 { k[0] = 1 }
67 u384_store_be(out_priv48, k)
68 return NX_P384_ECDH_OK
69}
70
71// out_pub97 = 0x04 || X(48) || Y(48) for priv * G.
72func p384_ecdh_pub(priv48: *u8, out_pub97: *u8) -> i64 {
73 let k: *i64 = u384_alloc()
74 let kv: i64 = _p384_ecdh_load_scalar(priv48, k)
75 if kv != NX_P384_ECDH_OK { return kv }
76
77 let G: *P384Point = p384_point_alloc()
78 p384_point_load_g(G)
79 let R: *P384Point = p384_point_alloc()
80 p384_scalar_mul(R, k, G)
81 if p384_point_is_infinity(R) == 1 { return NX_P384_ECDH_BAD_PRIV }
82 p384_point_to_affine(R)
83
84 out_pub97[0] = 4 as u8
85 u384_store_be(out_pub97 + 1, R.x)
86 u384_store_be(out_pub97 + 49, R.y)
87 return NX_P384_ECDH_OK
88}
89
90// Shared secret = x-coordinate of priv * peer_pub. peer_pub97 MUST be 0x04 || X || Y (97 bytes); the point is
91// checked on-curve + not-infinity before any scalar work (boundary validation: peer share arrives off the wire).
92func p384_ecdh_shared(priv48: *u8, peer_pub97: *u8, peer_len: i64, out_shared48: *u8) -> i64 {
93 if peer_len != 97 { return NX_P384_ECDH_BAD_POINT }
94 if peer_pub97[0] != (4 as u8) { return NX_P384_ECDH_BAD_POINT }
95
96 let px: *i64 = u384_alloc()
97 let py: *i64 = u384_alloc()
98 u384_load_be(px, peer_pub97 + 1)
99 u384_load_be(py, peer_pub97 + 49)
100
101 let P: *P384Point = p384_point_alloc()
102 p384_point_set_affine(P, px, py)
103 if p384_point_on_curve(P) != 1 { return NX_P384_ECDH_BAD_POINT }
104 if p384_point_is_infinity(P) == 1 { return NX_P384_ECDH_BAD_POINT }
105
106 let k: *i64 = u384_alloc()
107 let kv: i64 = _p384_ecdh_load_scalar(priv48, k)
108 if kv != NX_P384_ECDH_OK { return kv }
109
110 let R: *P384Point = p384_point_alloc()
111 p384_scalar_mul(R, k, P)
112 if p384_point_is_infinity(R) == 1 { return NX_P384_ECDH_BAD_POINT }
113 p384_point_to_affine(R)
114
115 u384_store_be(out_shared48, R.x)
116 return NX_P384_ECDH_OK
117}
118
119// Compile-only smoke.
120func main() -> i64 { return 0 }