code wiki / (root) / nx_p384_ecdh.nx

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 }