@gjsify/tls-native 0.4.29 → 0.4.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,344 @@
1
+ /*
2
+ * SessionAccess — POC scaffold for Phase 2 (TLS session resumption +
3
+ * channel binding) of @gjsify/tls-native.
4
+ *
5
+ * Status
6
+ * ──────
7
+ * This class delivers the JS-visible *surface* (so `@gjsify/tls` can
8
+ * wire `getFinished()` / `getPeerFinished()` / `getSession()` /
9
+ * `setSession()` / `isSessionReused()` and the `'session'` event into
10
+ * `TLSSocket` with proper typings and consistent error semantics), but
11
+ * the actual GnuTLS interactions throw a clear "not supported" error
12
+ * pending the GIO-internal struct-layout work documented in
13
+ * `docs/poc/tls-phase2-session-access.md`.
14
+ *
15
+ * Why a POC and not a real impl: GnuTLS's session-resumption +
16
+ * channel-binding APIs all operate on a `gnutls_session_t` handle,
17
+ * which `Gio.TlsConnection` keeps private inside the glib-networking
18
+ * GnuTLS backend (`_GTlsConnectionGnutlsPrivate`). Reaching it requires
19
+ * either:
20
+ *
21
+ * (a) Walking the GTypeInstance private-data offset for
22
+ * `GTlsConnectionGnutls` — needs the exact struct layout from a
23
+ * known glib-networking commit (see the docs/poc note).
24
+ * (b) A future upstream patch to expose a `g_tls_connection_get_native_session()`
25
+ * (or similar) accessor.
26
+ *
27
+ * Until either path is available, every native call here returns the
28
+ * `GError` domain `GjsifyTls.session_access_quark()`/code
29
+ * `NOT_SUPPORTED` with a message pointing at the docs/poc note. The
30
+ * JS side surfaces this as a regular `Error` thrown synchronously
31
+ * from the `@gjsify/tls` getter — so consumers can detect it via
32
+ * `try/catch` AND gate up-front via `hasTlsSessionAccess()`.
33
+ *
34
+ * Wiring the real implementation later is intentionally a small
35
+ * change: each `throw_not_supported()` site below becomes a call into
36
+ * a new `_native_session_for_connection()` helper that returns the
37
+ * raw `gnutls_session_t`, then the existing GnuTLS APIs (already in
38
+ * Vala 0.56's `gnutls.vapi`, with `session_channel_binding` added in
39
+ * the sibling `gnutls-session.vapi`) take over. The JS-side bindings
40
+ * + tests don't need to change.
41
+ *
42
+ * Phase 2 scope
43
+ * ─────────────
44
+ * Three Node-equivalent capabilities, all blocked on the same
45
+ * `gnutls_session_t` extraction:
46
+ *
47
+ * 1. Session resumption — `getSession()`/`setSession()` data
48
+ * buffer for `gnutls_session_get_data2` / `gnutls_session_set_data`,
49
+ * `isSessionReused()` for `gnutls_session_is_resumed`, and a
50
+ * `'session'` event hook (`'new-session-ticket'` signal proxy).
51
+ * 2. Channel binding — `getFinished()` / `getPeerFinished()` are
52
+ * Node-compat aliases for the `tls-unique` channel-binding
53
+ * bytes (RFC 5929 §3, TLS 1.0–1.2). For TLS 1.3 the same APIs
54
+ * semantically degrade to `tls-exporter` (RFC 9266) — covered by
55
+ * the `get_channel_binding()` method taking a binding type.
56
+ * 3. Negotiated-protocol introspection beyond what Gio surfaces —
57
+ * not strictly Phase 2 but tracked here so the next iteration
58
+ * doesn't duplicate the bridge.
59
+ */
60
+
61
+ using GLib;
62
+
63
+ namespace GjsifyTls {
64
+
65
+ /**
66
+ * Error domain for {@link SessionAccess} failures.
67
+ */
68
+ public errordomain SessionAccessError {
69
+ /**
70
+ * The underlying GIO/GnuTLS plumbing is not available — typically
71
+ * because the `gnutls_session_t` cannot be extracted from a
72
+ * `Gio.TlsConnection` in this GLib/glib-networking version.
73
+ * Track progress in `docs/poc/tls-phase2-session-access.md`.
74
+ */
75
+ NOT_SUPPORTED,
76
+ /**
77
+ * The supplied `Gio.TlsConnection` is null or the handshake has
78
+ * not yet completed — the GnuTLS session is not ready.
79
+ */
80
+ NOT_READY,
81
+ /**
82
+ * The GnuTLS call returned a non-zero error code. The {@link
83
+ * SessionAccessError.code} message includes the GnuTLS error
84
+ * string when available.
85
+ */
86
+ GNUTLS_ERROR,
87
+ }
88
+
89
+ /**
90
+ * Symbolic channel-binding selector for {@link SessionAccess.get_channel_binding}.
91
+ *
92
+ * Mirrors GnuTLS's `gnutls_channel_binding_t` so the JS layer can
93
+ * pass int constants without depending on the GnuTLS GIR (GnuTLS
94
+ * has no GIR; values are stable per RFC 5929 / RFC 9266 / GnuTLS
95
+ * API stability).
96
+ */
97
+ public enum ChannelBindingType {
98
+ /** `tls-unique` (RFC 5929 §3). TLS 1.0–1.2 only. */
99
+ TLS_UNIQUE = 0,
100
+ /** `tls-server-end-point` (RFC 5929 §4). */
101
+ TLS_SERVER_END_POINT = 1,
102
+ /** `tls-exporter` (RFC 9266). TLS 1.3 replacement for `tls-unique`. */
103
+ TLS_EXPORTER = 2,
104
+ }
105
+
106
+ /**
107
+ * SessionAccess — wraps a `Gio.TlsConnection` to extract / inject
108
+ * data that the GIO API does not expose: serialized session
109
+ * resumption blobs, channel-binding bytes for SCRAM-SHA-* SASL,
110
+ * and the `is_resumed` predicate.
111
+ *
112
+ * Construction
113
+ * ────────────
114
+ * Created via {@link SessionAccess.for_connection} — the binding
115
+ * holds a strong ref on the connection so the session stays alive
116
+ * for the bridge's lifetime. The connection MUST have completed a
117
+ * handshake before any of the extraction APIs are called; calling
118
+ * earlier yields {@link SessionAccessError.NOT_READY}.
119
+ *
120
+ * Native session pointer
121
+ * ──────────────────────
122
+ * Every method below resolves the `gnutls_session_t` via
123
+ * {@link _resolve_native_session}, which currently always returns
124
+ * `null` and triggers a {@link SessionAccessError.NOT_SUPPORTED}.
125
+ * When the GIO struct-layout question is resolved, that single
126
+ * function becomes the only file that changes — the public
127
+ * surface stays stable.
128
+ */
129
+ public class SessionAccess : GLib.Object {
130
+
131
+ /** Strong ref on the wrapped connection.
132
+ *
133
+ * Note: Vala maps GIO into the `GLib` namespace (see
134
+ * `gio-2.0.vapi` — `[CCode (gir_namespace = "Gio")]`
135
+ * `namespace GLib {`). So `Gio.TlsConnection` in JS / GIR
136
+ * corresponds to `GLib.TlsConnection` in Vala. The class
137
+ * surface published to JS via the GIR still appears as
138
+ * `Gio.TlsConnection` (the gir_namespace attribute drives
139
+ * the introspection output). */
140
+ private GLib.TlsConnection _connection;
141
+
142
+ /**
143
+ * Returns whether SessionAccess is functional in this runtime.
144
+ *
145
+ * Today this always returns `false` (Phase 2 POC). When the
146
+ * GIO-internal `gnutls_session_t` access lands, this returns
147
+ * `true` for connections whose backend is the GnuTLS one
148
+ * (`GTlsConnectionGnutls` instance) and `false` for any other
149
+ * (e.g. a hypothetical OpenSSL backend, or a mock).
150
+ *
151
+ * Consumers should call this BEFORE constructing a
152
+ * {@link SessionAccess} — passing the result through to
153
+ * `hasTlsSessionAccess()` on the JS side.
154
+ */
155
+ public static bool is_supported () {
156
+ // POC: always false. See `_resolve_native_session()` below.
157
+ return false;
158
+ }
159
+
160
+ /**
161
+ * Build a SessionAccess for a live `Gio.TlsConnection`. The
162
+ * connection is retained until this object is collected.
163
+ *
164
+ * Returns `null` if @connection is `null` — callers can use
165
+ * the null-coalescing pattern to short-circuit.
166
+ */
167
+ public static SessionAccess? for_connection (GLib.TlsConnection? connection) {
168
+ if (connection == null) {
169
+ return null;
170
+ }
171
+ return new SessionAccess.with_connection (connection);
172
+ }
173
+
174
+ /** Internal constructor — use {@link for_connection}. */
175
+ private SessionAccess.with_connection (GLib.TlsConnection connection) {
176
+ this._connection = connection;
177
+ }
178
+
179
+ /**
180
+ * Returns `true` if the underlying TLS session was resumed
181
+ * from a session ticket or session ID rather than completing
182
+ * a full handshake.
183
+ *
184
+ * Wraps `gnutls_session_is_resumed`.
185
+ *
186
+ * @throws SessionAccessError if the native session cannot be
187
+ * accessed (currently: always, until the Phase 2
188
+ * struct-layout work lands).
189
+ */
190
+ public bool is_session_reused () throws SessionAccessError {
191
+ _resolve_native_session ();
192
+ // Unreachable today — _resolve_native_session() always throws.
193
+ return false;
194
+ }
195
+
196
+ /**
197
+ * Extract the serialized session-resumption blob from the
198
+ * current handshake. Suitable for stashing in a JS variable
199
+ * and feeding back into a subsequent connect call via
200
+ * {@link set_session_data} (the `{session}` option on the
201
+ * Node-side `tls.connect()`).
202
+ *
203
+ * Wraps `gnutls_session_get_data2`.
204
+ *
205
+ * @returns serialized session as `Bytes` on success.
206
+ * @throws SessionAccessError if the native session cannot be
207
+ * accessed.
208
+ */
209
+ public GLib.Bytes get_session_data () throws SessionAccessError {
210
+ _resolve_native_session ();
211
+ return new GLib.Bytes (new uint8[0]);
212
+ }
213
+
214
+ /**
215
+ * Inject a previously serialized session blob to attempt
216
+ * resumption. Must be called BEFORE the handshake completes
217
+ * — typically right after `Gio.TlsClientConnection.new()` and
218
+ * before `handshake_async()`.
219
+ *
220
+ * Wraps `gnutls_session_set_data`.
221
+ *
222
+ * @param data serialized blob from a prior {@link get_session_data} call.
223
+ * @throws SessionAccessError if the native session cannot be
224
+ * accessed.
225
+ */
226
+ public void set_session_data (GLib.Bytes data) throws SessionAccessError {
227
+ _resolve_native_session ();
228
+ }
229
+
230
+ /**
231
+ * Extract the TLS-Finished bytes for the given channel-binding
232
+ * type. The default (`TLS_UNIQUE`) matches Node's
233
+ * `tlsSocket.getFinished()` semantics; `TLS_EXPORTER` is the
234
+ * TLS 1.3 replacement.
235
+ *
236
+ * Wraps `gnutls_session_channel_binding`.
237
+ *
238
+ * @param binding the binding type — see {@link ChannelBindingType}.
239
+ * @returns the binding bytes on success.
240
+ * @throws SessionAccessError if the native session cannot be
241
+ * accessed or the binding type is not supported by
242
+ * the negotiated TLS version.
243
+ */
244
+ public GLib.Bytes get_channel_binding (ChannelBindingType binding = ChannelBindingType.TLS_UNIQUE)
245
+ throws SessionAccessError {
246
+ _resolve_native_session ();
247
+ return new GLib.Bytes (new uint8[0]);
248
+ }
249
+
250
+ /**
251
+ * Convenience: `getFinished()` per Node's TLSSocket API.
252
+ * Returns the local Finished bytes (i.e. the bytes WE sent).
253
+ * On TLS 1.3 returns the `tls-exporter` material instead.
254
+ *
255
+ * Same blocker as {@link get_channel_binding}.
256
+ */
257
+ public GLib.Bytes get_finished () throws SessionAccessError {
258
+ // TLS 1.3 vs 1.2 selection happens here once
259
+ // _resolve_native_session() returns a real handle — for
260
+ // 1.3 we'd call get_channel_binding(TLS_EXPORTER) and
261
+ // for ≤1.2 get_channel_binding(TLS_UNIQUE). Today the
262
+ // throw is unconditional.
263
+ return get_channel_binding (ChannelBindingType.TLS_UNIQUE);
264
+ }
265
+
266
+ /**
267
+ * Convenience: `getPeerFinished()` per Node's TLSSocket API.
268
+ * Returns the peer's Finished bytes. Same TLS 1.3 fallback as
269
+ * {@link get_finished}.
270
+ *
271
+ * Implementation note: GnuTLS's `gnutls_session_channel_binding`
272
+ * with `TLS_UNIQUE` returns the local-side Finished. For the
273
+ * peer-side bytes we need a sibling call that reads the
274
+ * remote Finished from the same session state — exists as
275
+ * `gnutls_session_get_random_*` + manual derivation, OR via
276
+ * the same TLS_UNIQUE binding bytes which by design are
277
+ * symmetric for both peers on the same session (the binding
278
+ * is shared, not directional). Node distinguishes
279
+ * `getFinished()` vs `getPeerFinished()` because OpenSSL
280
+ * exposes both halves separately; for SCRAM-SHA-* the value
281
+ * actually used is `tls-unique` (shared) so the distinction
282
+ * is informational. See docs/poc note for the precise
283
+ * mapping the Path-A implementation will use.
284
+ */
285
+ public GLib.Bytes get_peer_finished () throws SessionAccessError {
286
+ return get_channel_binding (ChannelBindingType.TLS_UNIQUE);
287
+ }
288
+
289
+ /**
290
+ * Get the protocol version actually negotiated, as a stable
291
+ * string. Mirrors `Gio.TlsConnection.get_protocol_version()`
292
+ * but is colocated here so the JS-side `SessionAccess`
293
+ * wrapper has one consistent surface — useful when the
294
+ * `tls-unique` vs `tls-exporter` switch in
295
+ * {@link get_finished} needs the live version.
296
+ *
297
+ * Unlike the rest of this class, this method DOES return a
298
+ * useful value today — it reads `Gio.TlsConnection.get_protocol_version()`
299
+ * directly. POC value: lets tests / consumers exercise the
300
+ * SessionAccess shape end-to-end even before the gnutls
301
+ * blocker is resolved.
302
+ */
303
+ public string get_negotiated_protocol_version () {
304
+ var proto = _connection.get_protocol_version ();
305
+ switch (proto) {
306
+ case GLib.TlsProtocolVersion.TLS_1_0: return "TLSv1";
307
+ case GLib.TlsProtocolVersion.TLS_1_1: return "TLSv1.1";
308
+ case GLib.TlsProtocolVersion.TLS_1_2: return "TLSv1.2";
309
+ case GLib.TlsProtocolVersion.TLS_1_3: return "TLSv1.3";
310
+ default: return "unknown";
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Resolve the `gnutls_session_t` for {@link _connection}.
316
+ *
317
+ * Today this always throws {@link SessionAccessError.NOT_SUPPORTED}.
318
+ * The eventual implementation needs to:
319
+ *
320
+ * 1. Confirm `_connection` is a `GTlsConnectionGnutls`
321
+ * instance (`g_type_check_instance_is_a` against the
322
+ * type from `gio-tls-gnutls.so`).
323
+ * 2. Read the `session` field from
324
+ * `((GTlsConnectionGnutls*)_connection)->priv->session`
325
+ * — the layout comes from `glib-networking/tls/gnutls/
326
+ * gtlsconnection-gnutls-base.c`.
327
+ * 3. Return the pointer as `void*` so the GnuTLS calls in
328
+ * the methods above can use it.
329
+ *
330
+ * Until that lands, the unconditional throw keeps the API
331
+ * surface honest. See docs/poc/tls-phase2-session-access.md
332
+ * for the open questions and acceptance criteria for Path A.
333
+ */
334
+ private void* _resolve_native_session () throws SessionAccessError {
335
+ throw new SessionAccessError.NOT_SUPPORTED (
336
+ "@gjsify/tls-native SessionAccess: extracting gnutls_session_t " +
337
+ "from Gio.TlsConnection is not yet implemented. The Phase 2 " +
338
+ "native bits ship as a POC scaffold; see " +
339
+ "docs/poc/tls-phase2-session-access.md for the open struct-layout " +
340
+ "question that gates the real implementation."
341
+ );
342
+ }
343
+ }
344
+ }