@gjsify/tls-native 0.4.31 → 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.
@@ -1 +1 @@
1
- import{__name as e}from"./_virtual/_rolldown/runtime.js";import{OcspCertStatus as t,OcspResponseStatus as n,TlsChannelBindingType as r,createSessionAccess as i,hasNativeTls as a,hasTlsSessionAccess as o,nativeTls as s,parseOcspResponse as c}from"./index.js";import{describe as l,expect as u,it as d,on as f}from"@gjsify/unit";var p=e(async()=>{await f(`Gjs`,async()=>{await l(`@gjsify/tls-native — module loading`,async()=>{await d(`loads the GjsifyTls typelib successfully`,()=>{u(a()).toBe(!0),u(s).not.toBeNull(),u(typeof s?.Tls.parse_ocsp_response).toBe(`function`)})}),await l(`@gjsify/tls-native — parse_ocsp_response`,async()=>{await d(`returns null for empty input`,()=>{u(c(new Uint8Array)).toBeNull()}),await d(`returns null for non-OCSP garbage bytes`,()=>{u(c(new Uint8Array([255,255,255,255,1,2,3]))).toBeNull()}),await d(`returns null for truncated DER (sequence header only)`,()=>{u(c(new Uint8Array([48,1,0]))).toBeNull()})}),await l(`@gjsify/tls-native — symbolic constants`,async()=>{await d(`exposes OcspCertStatus enum-style constants`,()=>{u(t.GOOD).toBe(0),u(t.REVOKED).toBe(1),u(t.UNKNOWN).toBe(2)}),await d(`exposes OcspResponseStatus enum-style constants`,()=>{u(n.SUCCESSFUL).toBe(0),u(n.MALFORMED_REQUEST).toBe(1),u(n.INTERNAL_ERROR).toBe(2),u(n.TRY_LATER).toBe(3),u(n.SIG_REQUIRED).toBe(5),u(n.UNAUTHORIZED).toBe(6)}),await d(`exposes TlsChannelBindingType enum-style constants`,()=>{u(r.TLS_UNIQUE).toBe(0),u(r.TLS_SERVER_END_POINT).toBe(1),u(r.TLS_EXPORTER).toBe(2)})}),await l(`@gjsify/tls-native — SessionAccess (Phase 2 POC)`,async()=>{await d(`exposes the SessionAccess class on the native module`,()=>{u(s).not.toBeNull(),u(typeof s?.SessionAccess.is_supported).toBe(`function`),u(typeof s?.SessionAccess.for_connection).toBe(`function`)}),await d(`hasTlsSessionAccess() returns false until the gnutls_session_t access lands`,()=>{u(o()).toBe(!1)}),await d(`SessionAccess.is_supported() matches hasTlsSessionAccess()`,()=>{u(s?.SessionAccess.is_supported()).toBe(!1)}),await d(`createSessionAccess(null) returns null`,()=>{u(i(null)).toBeNull()}),await d(`SessionAccess.for_connection(null) returns null`,()=>{u(s?.SessionAccess.for_connection(null)).toBeNull()})})})},`default`);export{p as default};
1
+ import{__name as e}from"./_virtual/_rolldown/runtime.js";import{OcspCertStatus as t,OcspResponseStatus as n,TlsChannelBindingType as r,createSessionAccess as i,hasNativeTls as a,hasTlsSessionAccess as o,nativeTls as s,parseOcspResponse as c}from"./index.js";import{describe as l,expect as u,it as d,on as f}from"@gjsify/unit";var p=e(async()=>{await f(`Gjs`,async()=>{await l(`@gjsify/tls-native — module loading`,async()=>{await d(`loads the GjsifyTls typelib successfully`,()=>{u(a()).toBe(!0),u(s).not.toBeNull(),u(typeof s?.Tls.parse_ocsp_response).toBe(`function`)})}),await l(`@gjsify/tls-native — parse_ocsp_response`,async()=>{await d(`returns null for empty input`,()=>{u(c(new Uint8Array)).toBeNull()}),await d(`returns null for non-OCSP garbage bytes`,()=>{u(c(new Uint8Array([255,255,255,255,1,2,3]))).toBeNull()}),await d(`returns null for truncated DER (sequence header only)`,()=>{u(c(new Uint8Array([48,1,0]))).toBeNull()})}),await l(`@gjsify/tls-native — symbolic constants`,async()=>{await d(`exposes OcspCertStatus enum-style constants`,()=>{u(t.GOOD).toBe(0),u(t.REVOKED).toBe(1),u(t.UNKNOWN).toBe(2)}),await d(`exposes OcspResponseStatus enum-style constants`,()=>{u(n.SUCCESSFUL).toBe(0),u(n.MALFORMED_REQUEST).toBe(1),u(n.INTERNAL_ERROR).toBe(2),u(n.TRY_LATER).toBe(3),u(n.SIG_REQUIRED).toBe(5),u(n.UNAUTHORIZED).toBe(6)}),await d(`exposes TlsChannelBindingType enum-style constants`,()=>{u(r.TLS_UNIQUE).toBe(0),u(r.TLS_SERVER_END_POINT).toBe(1),u(r.TLS_EXPORTER).toBe(2)})}),await l(`@gjsify/tls-native — SessionAccess (Phase 2)`,async()=>{await d(`exposes the SessionAccess class on the native module`,()=>{u(s).not.toBeNull(),u(typeof s?.SessionAccess.is_supported).toBe(`function`),u(typeof s?.SessionAccess.for_connection).toBe(`function`)}),await d(`hasTlsSessionAccess() returns true under glib-networking GnuTLS backend`,()=>{u(o()).toBe(!0)}),await d(`SessionAccess.is_supported() matches hasTlsSessionAccess()`,()=>{u(s?.SessionAccess.is_supported()).toBe(!0)}),await d(`createSessionAccess(null) returns null`,()=>{u(i(null)).toBeNull()}),await d(`SessionAccess.for_connection(null) returns null`,()=>{u(s?.SessionAccess.for_connection(null)).toBeNull()})})})},`default`);export{p as default};
package/meson.build CHANGED
@@ -12,20 +12,33 @@ dependencies = [
12
12
  sources = files(
13
13
  'src/vala/tls.vala',
14
14
  'src/vala/session-access.vala',
15
+ # Phase-2 Path-A C shim. Compiled into the same shared lib so
16
+ # SessionAccess can delegate without an additional dlopen step.
17
+ # The header lives under `src/c/` and is reached via the
18
+ # `c_args: ['-I…/src/c']` flag below.
19
+ 'src/c/gjsify-tls-private.c',
15
20
  )
16
21
 
17
- # Sibling vapis: `gnutls-ocsp.vapi` (Phase 1 OCSP parser) and
18
- # `gnutls-session.vapi` (Phase 2 channel-binding API). The bundled
19
- # Vala 0.56 gnutls.vapi has session-data set/get/get2 and is_resumed
20
- # already, but not `gnutls_session_channel_binding` — added in the
21
- # sibling vapi.
22
+ # Sibling vapis:
23
+ # gnutls-ocsp.vapi Phase 1 OCSP parser bindings
24
+ # gnutls-session.vapi channel-binding enum + function
25
+ # (Vala 0.56's bundled gnutls.vapi has
26
+ # session_set/get/get2/is_resumed already,
27
+ # but lacks gnutls_session_channel_binding)
28
+ # gjsify-tls-private.vapi — Path-A C-shim bindings
29
+ # (session_t extraction + GnuTLS-call
30
+ # wrappers + GError mapping)
22
31
  libGjsifyTls = library('gjsifytls', sources,
23
32
  dependencies: dependencies,
33
+ c_args: [
34
+ '-I' + meson.current_source_dir() / 'src/c',
35
+ ],
24
36
  vala_args: [
25
37
  '--pkg=gnutls',
26
38
  '--vapidir=' + meson.current_source_dir() / 'src/vala',
27
39
  '--pkg=gnutls-ocsp',
28
40
  '--pkg=gnutls-session',
41
+ '--pkg=gjsify-tls-private',
29
42
  ],
30
43
  vala_gir: meson.project_name() + '-1.0.gir',
31
44
  install: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/tls-native",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "Optional Vala/GObject bridge providing GnuTLS capabilities not exposed by Gio.TlsConnection — OCSP-response parsing (RFC 6960), session resumption data extraction, channel binding (tls-finished bytes for SCRAM-SHA-*). Enhances @gjsify/tls when installed.",
5
5
  "type": "module",
6
6
  "main": "lib/esm/index.js",
@@ -19,7 +19,12 @@
19
19
  "src/vala"
20
20
  ],
21
21
  "gjsify": {
22
- "prebuilds": "prebuilds"
22
+ "prebuilds": "prebuilds",
23
+ "runtimes": {
24
+ "gjs": "polyfill",
25
+ "node": "none",
26
+ "browser": "none"
27
+ }
23
28
  },
24
29
  "keywords": [
25
30
  "gjs",
@@ -53,8 +58,8 @@
53
58
  "@girs/gobject-2.0": "2.88.0-4.0.1"
54
59
  },
55
60
  "devDependencies": {
56
- "@gjsify/cli": "^0.4.31",
57
- "@gjsify/unit": "^0.4.31",
61
+ "@gjsify/cli": "^0.4.32",
62
+ "@gjsify/unit": "^0.4.32",
58
63
  "@ts-for-gir/cli": "^4.0.0-rc.15",
59
64
  "@types/node": "^25.9.1",
60
65
  "typescript": "^6.0.3"
@@ -0,0 +1,39 @@
1
+ /* gjsify-tls-private.vapi — Vala binding for the local C shim that
2
+ * exposes `Gio.TlsConnection` GnuTLS-session-t-backed APIs (session
3
+ * resumption, channel binding). See `src/c/gjsify-tls-private.h` for
4
+ * the documented surface.
5
+ *
6
+ * Loaded via meson's `vala_args: ['--vapidir=<srcdir>/src/vala']`.
7
+ *
8
+ * Used by: `session-access.vala` Path-A implementation (replacing
9
+ * the POC NOT_SUPPORTED throws with real GnuTLS-backed calls).
10
+ */
11
+
12
+ [CCode (cheader_filename = "gjsify-tls-private.h", cprefix = "gjsify_tls_private_")]
13
+ namespace GjsifyTlsPrivate {
14
+
15
+ [CCode (cname = "GjsifyTlsPrivateError", cprefix = "GJSIFY_TLS_PRIVATE_ERROR_", has_type_id = false)]
16
+ public errordomain Error {
17
+ NOT_SUPPORTED,
18
+ GNUTLS_FAILED;
19
+ public static GLib.Quark quark ();
20
+ }
21
+
22
+ [CCode (cname = "gjsify_tls_private_is_supported")]
23
+ public bool is_supported ();
24
+
25
+ [CCode (cname = "gjsify_tls_private_is_gnutls_connection")]
26
+ public bool is_gnutls_connection (GLib.TlsConnection conn);
27
+
28
+ [CCode (cname = "gjsify_tls_private_is_session_reused")]
29
+ public bool is_session_reused (GLib.TlsConnection conn) throws GjsifyTlsPrivate.Error;
30
+
31
+ [CCode (cname = "gjsify_tls_private_get_session_data")]
32
+ public GLib.Bytes get_session_data (GLib.TlsConnection conn) throws GjsifyTlsPrivate.Error;
33
+
34
+ [CCode (cname = "gjsify_tls_private_set_session_data")]
35
+ public bool set_session_data (GLib.TlsConnection conn, GLib.Bytes data) throws GjsifyTlsPrivate.Error;
36
+
37
+ [CCode (cname = "gjsify_tls_private_get_channel_binding")]
38
+ public GLib.Bytes get_channel_binding (GLib.TlsConnection conn, int binding_type) throws GjsifyTlsPrivate.Error;
39
+ }
@@ -1,43 +1,28 @@
1
1
  /*
2
- * SessionAccess — POC scaffold for Phase 2 (TLS session resumption +
3
- * channel binding) of @gjsify/tls-native.
2
+ * SessionAccess — Phase 2 (TLS session resumption + channel binding)
3
+ * of @gjsify/tls-native.
4
4
  *
5
5
  * Status
6
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`.
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).
14
15
  *
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.
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.
41
26
  *
42
27
  * Phase 2 scope
43
28
  * ─────────────
@@ -142,19 +127,18 @@ namespace GjsifyTls {
142
127
  /**
143
128
  * Returns whether SessionAccess is functional in this runtime.
144
129
  *
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).
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).
150
135
  *
151
136
  * Consumers should call this BEFORE constructing a
152
137
  * {@link SessionAccess} — passing the result through to
153
138
  * `hasTlsSessionAccess()` on the JS side.
154
139
  */
155
140
  public static bool is_supported () {
156
- // POC: always false. See `_resolve_native_session()` below.
157
- return false;
141
+ return GjsifyTlsPrivate.is_supported ();
158
142
  }
159
143
 
160
144
  /**
@@ -183,14 +167,16 @@ namespace GjsifyTls {
183
167
  *
184
168
  * Wraps `gnutls_session_is_resumed`.
185
169
  *
186
- * @throws SessionAccessError if the native session cannot be
187
- * accessed (currently: always, until the Phase 2
188
- * struct-layout work lands).
170
+ * @throws SessionAccessError if the connection is not from
171
+ * the GnuTLS backend (`NOT_SUPPORTED`) or the GnuTLS
172
+ * API itself failed (`GNUTLS_ERROR`).
189
173
  */
190
174
  public bool is_session_reused () throws SessionAccessError {
191
- _resolve_native_session ();
192
- // Unreachable today — _resolve_native_session() always throws.
193
- return false;
175
+ try {
176
+ return GjsifyTlsPrivate.is_session_reused (this._connection);
177
+ } catch (GjsifyTlsPrivate.Error e) {
178
+ throw _wrap (e);
179
+ }
194
180
  }
195
181
 
196
182
  /**
@@ -207,8 +193,11 @@ namespace GjsifyTls {
207
193
  * accessed.
208
194
  */
209
195
  public GLib.Bytes get_session_data () throws SessionAccessError {
210
- _resolve_native_session ();
211
- return new GLib.Bytes (new uint8[0]);
196
+ try {
197
+ return GjsifyTlsPrivate.get_session_data (this._connection);
198
+ } catch (GjsifyTlsPrivate.Error e) {
199
+ throw _wrap (e);
200
+ }
212
201
  }
213
202
 
214
203
  /**
@@ -224,7 +213,11 @@ namespace GjsifyTls {
224
213
  * accessed.
225
214
  */
226
215
  public void set_session_data (GLib.Bytes data) throws SessionAccessError {
227
- _resolve_native_session ();
216
+ try {
217
+ GjsifyTlsPrivate.set_session_data (this._connection, data);
218
+ } catch (GjsifyTlsPrivate.Error e) {
219
+ throw _wrap (e);
220
+ }
228
221
  }
229
222
 
230
223
  /**
@@ -243,8 +236,11 @@ namespace GjsifyTls {
243
236
  */
244
237
  public GLib.Bytes get_channel_binding (ChannelBindingType binding = ChannelBindingType.TLS_UNIQUE)
245
238
  throws SessionAccessError {
246
- _resolve_native_session ();
247
- return new GLib.Bytes (new uint8[0]);
239
+ try {
240
+ return GjsifyTlsPrivate.get_channel_binding (this._connection, (int) binding);
241
+ } catch (GjsifyTlsPrivate.Error e) {
242
+ throw _wrap (e);
243
+ }
248
244
  }
249
245
 
250
246
  /**
@@ -255,12 +251,15 @@ namespace GjsifyTls {
255
251
  * Same blocker as {@link get_channel_binding}.
256
252
  */
257
253
  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);
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);
264
263
  }
265
264
 
266
265
  /**
@@ -283,7 +282,14 @@ namespace GjsifyTls {
283
282
  * mapping the Path-A implementation will use.
284
283
  */
285
284
  public GLib.Bytes get_peer_finished () throws SessionAccessError {
286
- return get_channel_binding (ChannelBindingType.TLS_UNIQUE);
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 ();
287
293
  }
288
294
 
289
295
  /**
@@ -312,33 +318,18 @@ namespace GjsifyTls {
312
318
  }
313
319
 
314
320
  /**
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.
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}.
333
327
  */
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
- );
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);
342
333
  }
343
334
  }
344
335
  }