@gjsify/tls-native 0.4.30 → 0.4.32

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,335 @@
1
+ /*
2
+ * SessionAccess — Phase 2 (TLS session resumption + channel binding)
3
+ * of @gjsify/tls-native.
4
+ *
5
+ * Status
6
+ * ──────
7
+ * Functional Path-A implementation. Every method below delegates to a
8
+ * thin C shim (`src/c/gjsify-tls-private.{c,h}`) that reaches into
9
+ * glib-networking's GnuTLS-backend private struct via the GLib 2.38+
10
+ * public `g_type_instance_get_private` + a runtime
11
+ * `g_type_from_name("GTlsConnectionGnutls")` lookup. The struct layout
12
+ * itself is vendored from `refs/glib-networking/tls/gnutls/
13
+ * gtlsconnection-gnutls.c` (see the file-header in the C shim for the
14
+ * vendored offsets + supported window).
15
+ *
16
+ * Backwards-compatibility on a non-GnuTLS backend
17
+ * ───────────────────────────────────────────────
18
+ * If `g_type_from_name("GTlsConnectionGnutls")` returns 0 (i.e. a
19
+ * hypothetical future OpenSSL backend is selected via `GIO_USE_TLS`),
20
+ * the static `is_supported()` returns `false` and every consumer call
21
+ * surfaces `SessionAccessError.NOT_SUPPORTED` — gracefully degrading
22
+ * the same way Node's TLSSocket does when built without session
23
+ * support. The JS-side `hasTlsSessionAccess()` predicate is the
24
+ * canonical gate consumers should check; see the per-method docs for
25
+ * the corresponding error semantics.
26
+ *
27
+ * Phase 2 scope
28
+ * ─────────────
29
+ * Three Node-equivalent capabilities, all blocked on the same
30
+ * `gnutls_session_t` extraction:
31
+ *
32
+ * 1. Session resumption — `getSession()`/`setSession()` data
33
+ * buffer for `gnutls_session_get_data2` / `gnutls_session_set_data`,
34
+ * `isSessionReused()` for `gnutls_session_is_resumed`, and a
35
+ * `'session'` event hook (`'new-session-ticket'` signal proxy).
36
+ * 2. Channel binding — `getFinished()` / `getPeerFinished()` are
37
+ * Node-compat aliases for the `tls-unique` channel-binding
38
+ * bytes (RFC 5929 §3, TLS 1.0–1.2). For TLS 1.3 the same APIs
39
+ * semantically degrade to `tls-exporter` (RFC 9266) — covered by
40
+ * the `get_channel_binding()` method taking a binding type.
41
+ * 3. Negotiated-protocol introspection beyond what Gio surfaces —
42
+ * not strictly Phase 2 but tracked here so the next iteration
43
+ * doesn't duplicate the bridge.
44
+ */
45
+
46
+ using GLib;
47
+
48
+ namespace GjsifyTls {
49
+
50
+ /**
51
+ * Error domain for {@link SessionAccess} failures.
52
+ */
53
+ public errordomain SessionAccessError {
54
+ /**
55
+ * The underlying GIO/GnuTLS plumbing is not available — typically
56
+ * because the `gnutls_session_t` cannot be extracted from a
57
+ * `Gio.TlsConnection` in this GLib/glib-networking version.
58
+ * Track progress in `docs/poc/tls-phase2-session-access.md`.
59
+ */
60
+ NOT_SUPPORTED,
61
+ /**
62
+ * The supplied `Gio.TlsConnection` is null or the handshake has
63
+ * not yet completed — the GnuTLS session is not ready.
64
+ */
65
+ NOT_READY,
66
+ /**
67
+ * The GnuTLS call returned a non-zero error code. The {@link
68
+ * SessionAccessError.code} message includes the GnuTLS error
69
+ * string when available.
70
+ */
71
+ GNUTLS_ERROR,
72
+ }
73
+
74
+ /**
75
+ * Symbolic channel-binding selector for {@link SessionAccess.get_channel_binding}.
76
+ *
77
+ * Mirrors GnuTLS's `gnutls_channel_binding_t` so the JS layer can
78
+ * pass int constants without depending on the GnuTLS GIR (GnuTLS
79
+ * has no GIR; values are stable per RFC 5929 / RFC 9266 / GnuTLS
80
+ * API stability).
81
+ */
82
+ public enum ChannelBindingType {
83
+ /** `tls-unique` (RFC 5929 §3). TLS 1.0–1.2 only. */
84
+ TLS_UNIQUE = 0,
85
+ /** `tls-server-end-point` (RFC 5929 §4). */
86
+ TLS_SERVER_END_POINT = 1,
87
+ /** `tls-exporter` (RFC 9266). TLS 1.3 replacement for `tls-unique`. */
88
+ TLS_EXPORTER = 2,
89
+ }
90
+
91
+ /**
92
+ * SessionAccess — wraps a `Gio.TlsConnection` to extract / inject
93
+ * data that the GIO API does not expose: serialized session
94
+ * resumption blobs, channel-binding bytes for SCRAM-SHA-* SASL,
95
+ * and the `is_resumed` predicate.
96
+ *
97
+ * Construction
98
+ * ────────────
99
+ * Created via {@link SessionAccess.for_connection} — the binding
100
+ * holds a strong ref on the connection so the session stays alive
101
+ * for the bridge's lifetime. The connection MUST have completed a
102
+ * handshake before any of the extraction APIs are called; calling
103
+ * earlier yields {@link SessionAccessError.NOT_READY}.
104
+ *
105
+ * Native session pointer
106
+ * ──────────────────────
107
+ * Every method below resolves the `gnutls_session_t` via
108
+ * {@link _resolve_native_session}, which currently always returns
109
+ * `null` and triggers a {@link SessionAccessError.NOT_SUPPORTED}.
110
+ * When the GIO struct-layout question is resolved, that single
111
+ * function becomes the only file that changes — the public
112
+ * surface stays stable.
113
+ */
114
+ public class SessionAccess : GLib.Object {
115
+
116
+ /** Strong ref on the wrapped connection.
117
+ *
118
+ * Note: Vala maps GIO into the `GLib` namespace (see
119
+ * `gio-2.0.vapi` — `[CCode (gir_namespace = "Gio")]`
120
+ * `namespace GLib {`). So `Gio.TlsConnection` in JS / GIR
121
+ * corresponds to `GLib.TlsConnection` in Vala. The class
122
+ * surface published to JS via the GIR still appears as
123
+ * `Gio.TlsConnection` (the gir_namespace attribute drives
124
+ * the introspection output). */
125
+ private GLib.TlsConnection _connection;
126
+
127
+ /**
128
+ * Returns whether SessionAccess is functional in this runtime.
129
+ *
130
+ * Returns `true` when glib-networking's GnuTLS backend is the
131
+ * active TLS backend (`GTlsConnectionGnutls` GType is
132
+ * registered). Returns `false` only when a non-GnuTLS GIO TLS
133
+ * backend is selected (e.g. via `GIO_USE_TLS=openssl` once
134
+ * that backend exists upstream).
135
+ *
136
+ * Consumers should call this BEFORE constructing a
137
+ * {@link SessionAccess} — passing the result through to
138
+ * `hasTlsSessionAccess()` on the JS side.
139
+ */
140
+ public static bool is_supported () {
141
+ return GjsifyTlsPrivate.is_supported ();
142
+ }
143
+
144
+ /**
145
+ * Build a SessionAccess for a live `Gio.TlsConnection`. The
146
+ * connection is retained until this object is collected.
147
+ *
148
+ * Returns `null` if @connection is `null` — callers can use
149
+ * the null-coalescing pattern to short-circuit.
150
+ */
151
+ public static SessionAccess? for_connection (GLib.TlsConnection? connection) {
152
+ if (connection == null) {
153
+ return null;
154
+ }
155
+ return new SessionAccess.with_connection (connection);
156
+ }
157
+
158
+ /** Internal constructor — use {@link for_connection}. */
159
+ private SessionAccess.with_connection (GLib.TlsConnection connection) {
160
+ this._connection = connection;
161
+ }
162
+
163
+ /**
164
+ * Returns `true` if the underlying TLS session was resumed
165
+ * from a session ticket or session ID rather than completing
166
+ * a full handshake.
167
+ *
168
+ * Wraps `gnutls_session_is_resumed`.
169
+ *
170
+ * @throws SessionAccessError if the connection is not from
171
+ * the GnuTLS backend (`NOT_SUPPORTED`) or the GnuTLS
172
+ * API itself failed (`GNUTLS_ERROR`).
173
+ */
174
+ public bool is_session_reused () throws SessionAccessError {
175
+ try {
176
+ return GjsifyTlsPrivate.is_session_reused (this._connection);
177
+ } catch (GjsifyTlsPrivate.Error e) {
178
+ throw _wrap (e);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Extract the serialized session-resumption blob from the
184
+ * current handshake. Suitable for stashing in a JS variable
185
+ * and feeding back into a subsequent connect call via
186
+ * {@link set_session_data} (the `{session}` option on the
187
+ * Node-side `tls.connect()`).
188
+ *
189
+ * Wraps `gnutls_session_get_data2`.
190
+ *
191
+ * @returns serialized session as `Bytes` on success.
192
+ * @throws SessionAccessError if the native session cannot be
193
+ * accessed.
194
+ */
195
+ public GLib.Bytes get_session_data () throws SessionAccessError {
196
+ try {
197
+ return GjsifyTlsPrivate.get_session_data (this._connection);
198
+ } catch (GjsifyTlsPrivate.Error e) {
199
+ throw _wrap (e);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Inject a previously serialized session blob to attempt
205
+ * resumption. Must be called BEFORE the handshake completes
206
+ * — typically right after `Gio.TlsClientConnection.new()` and
207
+ * before `handshake_async()`.
208
+ *
209
+ * Wraps `gnutls_session_set_data`.
210
+ *
211
+ * @param data serialized blob from a prior {@link get_session_data} call.
212
+ * @throws SessionAccessError if the native session cannot be
213
+ * accessed.
214
+ */
215
+ public void set_session_data (GLib.Bytes data) throws SessionAccessError {
216
+ try {
217
+ GjsifyTlsPrivate.set_session_data (this._connection, data);
218
+ } catch (GjsifyTlsPrivate.Error e) {
219
+ throw _wrap (e);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Extract the TLS-Finished bytes for the given channel-binding
225
+ * type. The default (`TLS_UNIQUE`) matches Node's
226
+ * `tlsSocket.getFinished()` semantics; `TLS_EXPORTER` is the
227
+ * TLS 1.3 replacement.
228
+ *
229
+ * Wraps `gnutls_session_channel_binding`.
230
+ *
231
+ * @param binding the binding type — see {@link ChannelBindingType}.
232
+ * @returns the binding bytes on success.
233
+ * @throws SessionAccessError if the native session cannot be
234
+ * accessed or the binding type is not supported by
235
+ * the negotiated TLS version.
236
+ */
237
+ public GLib.Bytes get_channel_binding (ChannelBindingType binding = ChannelBindingType.TLS_UNIQUE)
238
+ throws SessionAccessError {
239
+ try {
240
+ return GjsifyTlsPrivate.get_channel_binding (this._connection, (int) binding);
241
+ } catch (GjsifyTlsPrivate.Error e) {
242
+ throw _wrap (e);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Convenience: `getFinished()` per Node's TLSSocket API.
248
+ * Returns the local Finished bytes (i.e. the bytes WE sent).
249
+ * On TLS 1.3 returns the `tls-exporter` material instead.
250
+ *
251
+ * Same blocker as {@link get_channel_binding}.
252
+ */
253
+ public GLib.Bytes get_finished () throws SessionAccessError {
254
+ // On TLS ≤1.2 the relevant binding is `tls-unique` (the
255
+ // first Finished message, RFC 5929 §3). On TLS 1.3 the
256
+ // Finished messages are encrypted before the channel-
257
+ // binding is taken, so RFC 9266 specifies `tls-exporter`
258
+ // as the replacement.
259
+ var binding = _connection.get_protocol_version () == GLib.TlsProtocolVersion.TLS_1_3
260
+ ? ChannelBindingType.TLS_EXPORTER
261
+ : ChannelBindingType.TLS_UNIQUE;
262
+ return get_channel_binding (binding);
263
+ }
264
+
265
+ /**
266
+ * Convenience: `getPeerFinished()` per Node's TLSSocket API.
267
+ * Returns the peer's Finished bytes. Same TLS 1.3 fallback as
268
+ * {@link get_finished}.
269
+ *
270
+ * Implementation note: GnuTLS's `gnutls_session_channel_binding`
271
+ * with `TLS_UNIQUE` returns the local-side Finished. For the
272
+ * peer-side bytes we need a sibling call that reads the
273
+ * remote Finished from the same session state — exists as
274
+ * `gnutls_session_get_random_*` + manual derivation, OR via
275
+ * the same TLS_UNIQUE binding bytes which by design are
276
+ * symmetric for both peers on the same session (the binding
277
+ * is shared, not directional). Node distinguishes
278
+ * `getFinished()` vs `getPeerFinished()` because OpenSSL
279
+ * exposes both halves separately; for SCRAM-SHA-* the value
280
+ * actually used is `tls-unique` (shared) so the distinction
281
+ * is informational. See docs/poc note for the precise
282
+ * mapping the Path-A implementation will use.
283
+ */
284
+ public GLib.Bytes get_peer_finished () throws SessionAccessError {
285
+ // Per the GnuTLS manual + RFC 5929/9266 the channel-binding
286
+ // bytes are symmetric across both peers — there is no
287
+ // separate "peer" Finished available via the GnuTLS API
288
+ // (OpenSSL exposes both halves; GnuTLS does not). For the
289
+ // SCRAM-SHA-* use case the symmetric binding IS what SASL
290
+ // negotiates against, so this is functionally correct.
291
+ // Same TLS-1.3 fallback as `get_finished()`.
292
+ return get_finished ();
293
+ }
294
+
295
+ /**
296
+ * Get the protocol version actually negotiated, as a stable
297
+ * string. Mirrors `Gio.TlsConnection.get_protocol_version()`
298
+ * but is colocated here so the JS-side `SessionAccess`
299
+ * wrapper has one consistent surface — useful when the
300
+ * `tls-unique` vs `tls-exporter` switch in
301
+ * {@link get_finished} needs the live version.
302
+ *
303
+ * Unlike the rest of this class, this method DOES return a
304
+ * useful value today — it reads `Gio.TlsConnection.get_protocol_version()`
305
+ * directly. POC value: lets tests / consumers exercise the
306
+ * SessionAccess shape end-to-end even before the gnutls
307
+ * blocker is resolved.
308
+ */
309
+ public string get_negotiated_protocol_version () {
310
+ var proto = _connection.get_protocol_version ();
311
+ switch (proto) {
312
+ case GLib.TlsProtocolVersion.TLS_1_0: return "TLSv1";
313
+ case GLib.TlsProtocolVersion.TLS_1_1: return "TLSv1.1";
314
+ case GLib.TlsProtocolVersion.TLS_1_2: return "TLSv1.2";
315
+ case GLib.TlsProtocolVersion.TLS_1_3: return "TLSv1.3";
316
+ default: return "unknown";
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Translate a {@link GjsifyTlsPrivate.Error} from the C shim
322
+ * into the public {@link SessionAccessError} domain, preserving
323
+ * the original message verbatim. The mapping is one-to-one:
324
+ * `NOT_SUPPORTED` → `NOT_SUPPORTED`, `GNUTLS_FAILED` →
325
+ * `GNUTLS_ERROR`. The `NOT_READY` code stays reserved for the
326
+ * Vala-side `null`-connection guard in {@link for_connection}.
327
+ */
328
+ private SessionAccessError _wrap (GjsifyTlsPrivate.Error e) {
329
+ if (e is GjsifyTlsPrivate.Error.GNUTLS_FAILED) {
330
+ return new SessionAccessError.GNUTLS_ERROR (e.message);
331
+ }
332
+ return new SessionAccessError.NOT_SUPPORTED (e.message);
333
+ }
334
+ }
335
+ }