code wiki / (root) / nx_p256_ecdh.nx

nx_p256_ecdh.nx source

↩ module page · 144 lines · 5560 B

1// nx_p256_ecdh.nx -- ECDH key exchange on P-256 (secp256r1) for the 2// TLS 1.3 key_share path (browser arc rung B4-P256-KEYSHARE). 3// 4// The P-256 stack (field, point, scalar_mul) shipped for ECDSA cert 5// VERIFICATION; this module composes the same primitives into the 6// ECDHE role so the ClientHello can carry a secp256r1 KeyShareEntry 7// next to the X25519 one. Servers that insist on P-256 (the AWS- 8// fronted class behind the B2-HTTPBIN-CH alert) then never need a 9// HelloRetryRequest: they just pick our P-256 share. 10// 11// Public API: 12// p256_ecdh_derive_priv(seed32, out_priv32) -> verdict 13// Domain-separated ephemeral: SHA-256("NISHI-TLS13-P256-ECDH-V1" 14// || seed32), reduced mod n, forced nonzero. Lets the session 15// grow a P-256 keypair from the caller's existing 32 random 16// bytes without changing the session-new entropy contract. 17// p256_ecdh_pub(priv32, out_pub65) -> verdict 18// out = 0x04 || X || Y (RFC 8446 wire form for secp256r1). 19// p256_ecdh_shared(priv32, peer_pub65, peer_len, out_shared32) -> verdict 20// Validates peer point (format, on-curve, not-infinity) then 21// writes the x-coordinate of priv * peer as the 32-byte shared 22// secret (RFC 8446 section 7.4.2). 23// 24// Variable-time scalar_mul note: the scalar here is an EPHEMERAL 25// per-session secret, unlike ECDSA verify's public scalars. The 26// shipped double-and-add is variable-time; constant-time ladder is 27// a named follow-up rung (same lane as the x25519 ladder), accepted 28// for B4 because the scalar lives for one handshake. 29// 30// KAT: RFC 5903 section 8.1 vectors in nx_p256_keyshare_test.nx 31// (vectors re-read from rfc-editor.org 2026-06-10, not from memory). 32// 33// license_tier: INDEPENDENT_REDERIVE 34// genealogy_id: international-research-sources/ietf/rfc_5903 + ietf/rfc_8446 35// lineage_id: nishi_p256_ecdh_q10 36 37import "nx_syscalls.nx" 38import "nx_sha256.nx" 39import "nx_u256.nx" 40import "nx_p256_field.nx" 41import "nx_p256_point.nx" 42import "nx_p256_point_add.nx" 43import "nx_p256_scalar_mul.nx" 44import "nx_p256_modn.nx" 45 46const NX_P256_ECDH_OK: i64 = 1 47const NX_P256_ECDH_BAD_PRIV: i64 = 2 // scalar zero or >= n 48const NX_P256_ECDH_BAD_POINT: i64 = 3 // format/on-curve/infinity reject 49const NX_P256_ECDH_VERDICT_N: i64 = 4 50 51func nx_p256_ecdh_verdict_is_valid(v: i64) -> i64 { 52 if v < NX_P256_ECDH_OK { return 0 } 53 if v >= NX_P256_ECDH_VERDICT_N { return 0 } 54 return 1 55} 56 57// Load priv32 (big-endian) into limbs and range-check 0 < k < n. 58// Returns OK / BAD_PRIV; on OK the limbs are in out_k. 59func _p256_ecdh_load_scalar(priv32: *u8, out_k: *i64) -> i64 { 60 u256_load_be(out_k, priv32) 61 if u256_is_zero(out_k) == 1 { return NX_P256_ECDH_BAD_PRIV } 62 let n: *i64 = u256_alloc() 63 p256_modn_load_n(n) 64 if u256_cmp(out_k, n) >= 0 { return NX_P256_ECDH_BAD_PRIV } 65 return NX_P256_ECDH_OK 66} 67 68// Ephemeral P-256 private key from 32 seed bytes via domain- 69// separated SHA-256, conditionally reduced mod n (one subtract 70// suffices: n > 2^255 so any 256-bit digest is < 2n), forced 71// nonzero (probability 2^-256 branch, kept for totality). 72func p256_ecdh_derive_priv(seed32: *u8, out_priv32: *u8) -> i64 { 73 let tag: *u8 = "NISHI-TLS13-P256-ECDH-V1\x00" as *u8 74 let buf: *u8 = sys_mmap(64) 75 var i: i64 = 0 76 while i < 24 { buf[i] = tag[i]; i = i + 1 } 77 i = 0 78 while i < 32 { buf[24 + i] = seed32[i]; i = i + 1 } 79 let digest: *u8 = sys_mmap(32) 80 sha256_digest(buf, 56, digest) 81 82 let k: *i64 = u256_alloc() 83 u256_load_be(k, digest) 84 let n: *i64 = u256_alloc() 85 p256_modn_load_n(n) 86 if u256_cmp(k, n) >= 0 { u256_sub_with_borrow(k, k, n) } 87 if u256_is_zero(k) == 1 { k[0] = 1 } 88 u256_store_be(out_priv32, k) 89 return NX_P256_ECDH_OK 90} 91 92// out_pub65 = 0x04 || X(32) || Y(32) for priv * G. 93func p256_ecdh_pub(priv32: *u8, out_pub65: *u8) -> i64 { 94 let k: *i64 = u256_alloc() 95 let kv: i64 = _p256_ecdh_load_scalar(priv32, k) 96 if kv != NX_P256_ECDH_OK { return kv } 97 98 let G: *P256Point = p256_point_alloc() 99 p256_point_load_g(G) 100 let R: *P256Point = p256_point_alloc() 101 p256_scalar_mul(R, k, G) 102 if p256_point_is_infinity(R) == 1 { return NX_P256_ECDH_BAD_PRIV } 103 p256_point_to_affine(R) 104 105 out_pub65[0] = 4 as u8 106 u256_store_be(out_pub65 + 1, R.x) 107 u256_store_be(out_pub65 + 33, R.y) 108 return NX_P256_ECDH_OK 109} 110 111// Shared secret = x-coordinate of priv * peer_pub. 112// peer_pub65 MUST be a 65-byte uncompressed point (0x04 || X || Y); 113// the point is checked on-curve before any scalar work (boundary 114// validation: the peer share arrives off the wire). 115func p256_ecdh_shared( 116 priv32: *u8, 117 peer_pub65: *u8, peer_len: i64, 118 out_shared32: *u8 119) -> i64 { 120 if peer_len != 65 { return NX_P256_ECDH_BAD_POINT } 121 if peer_pub65[0] != (4 as u8) { return NX_P256_ECDH_BAD_POINT } 122 123 let px: *i64 = u256_alloc() 124 let py: *i64 = u256_alloc() 125 u256_load_be(px, peer_pub65 + 1) 126 u256_load_be(py, peer_pub65 + 33) 127 128 let P: *P256Point = p256_point_alloc() 129 p256_point_set_affine(P, px, py) 130 if p256_point_on_curve(P) != 1 { return NX_P256_ECDH_BAD_POINT } 131 if p256_point_is_infinity(P) == 1 { return NX_P256_ECDH_BAD_POINT } 132 133 let k: *i64 = u256_alloc() 134 let kv: i64 = _p256_ecdh_load_scalar(priv32, k) 135 if kv != NX_P256_ECDH_OK { return kv } 136 137 let R: *P256Point = p256_point_alloc() 138 p256_scalar_mul(R, k, P) 139 if p256_point_is_infinity(R) == 1 { return NX_P256_ECDH_BAD_POINT } 140 p256_point_to_affine(R) 141 142 u256_store_be(out_shared32, R.x) 143 return NX_P256_ECDH_OK 144}