@gjsify/tls 0.3.20 → 0.4.0

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 @@
1
+ var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
package/lib/esm/index.js CHANGED
@@ -1,3 +1,2 @@
1
- import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";import{Server as n,Socket as r}from"node:net";import{createNodeError as i,deferEmit as a}from"@gjsify/utils";const o=`TLSv1.2`,s=`TLSv1.3`,c=`TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384`;function l(){return[`aes-128-gcm`,`aes-256-gcm`,`chacha20-poly1305`,`aes-128-cbc`,`aes-256-cbc`]}function u(e){return e.endsWith(`.`)?e.slice(0,-1):e}function d(e){return u(e).toLowerCase().split(`.`)}function f(e,t){let n=d(t);return n.length===e.length?n[0]===`*`?n.slice(1).join(`.`)===e.slice(1).join(`.`):n.every((t,n)=>t===e[n]):!1}function p(e,t){let n=t.subject,r=t.subjectaltname,i=[],a=[];if(e=String(e),r){let e=r.split(`, `);for(let t of e)t.startsWith(`DNS:`)?i.push(t.slice(4)):t.startsWith(`IP Address:`)&&a.push(t.slice(11).trim())}let o=!1,s=`Unknown reason`;e=u(e);let c=/^(\d{1,3}\.){3}\d{1,3}$/.test(e),l=e.includes(`:`);if(c||l)o=a.some(t=>t.toLowerCase()===e.toLowerCase()),o||(s=`IP: ${e} is not in the cert's list: ${a.join(`, `)}`);else if(i.length>0||n?.CN){let t=d(e);if(i.length>0)o=i.some(e=>f(t,e)),o||(s=`Host: ${e}. is not in the cert's altnames: ${r}`);else{let r=n.CN;Array.isArray(r)?o=r.some(e=>f(t,e)):r&&(o=f(t,r)),o||(s=`Host: ${e}. is not cert's CN: ${r}`)}}else s=`Cert does not contain a DNS name`;if(!o){let n=Error(s);return n.reason=s,n.host=e,n.cert=t,n}}var m=class extends r{encrypted=!0;authorized=!1;authorizationError;alpnProtocol=!1;_tlsConnection=null;constructor(e,t){super()}_setupTlsStreams(e){this._tlsConnection=e,this._inputStream=e.get_input_stream(),this._outputStream=e.get_output_stream(),this._connection=e}getPeerCertificate(e){if(!this._tlsConnection)return{};try{return this._tlsConnection.get_peer_certificate()?{subject:{},issuer:{},valid_from:``,valid_to:``}:{}}catch{return{}}}getProtocol(){if(!this._tlsConnection)return null;try{switch(this._tlsConnection.get_protocol_version()){case e.TlsProtocolVersion.TLS_1_0:return`TLSv1`;case e.TlsProtocolVersion.TLS_1_1:return`TLSv1.1`;case e.TlsProtocolVersion.TLS_1_2:return`TLSv1.2`;case e.TlsProtocolVersion.TLS_1_3:return`TLSv1.3`;default:return null}}catch{return null}}getCipher(){if(!this._tlsConnection)return null;try{return{name:this._tlsConnection.get_ciphersuite_name()||`unknown`,version:this.getProtocol()||`unknown`}}catch{return null}}getAlpnProtocol(){if(!this._tlsConnection)return!1;try{return this._tlsConnection.get_negotiated_protocol()||!1}catch{return!1}}};function h(n,r){let i=new m(void 0,n);r&&i.once(`secureConnect`,r);let a=n.port||443,o=n.host||`localhost`,s=n.servername||o,c=n.rejectUnauthorized!==!1;return i.once(`connect`,()=>{let r=i._connection;if(!r){i.destroy(Error(`No underlying connection for TLS upgrade`));return}try{let o=e.NetworkAddress.new(s,a),l=e.TlsClientConnection.new(r,o);if(l.set_server_identity(o),n.ALPNProtocols&&n.ALPNProtocols.length>0)try{l.set_advertised_protocols(n.ALPNProtocols)}catch{}c||l.connect(`accept-certificate`,()=>!0);let u=new e.Cancellable;l.handshake_async(t.PRIORITY_DEFAULT,u,(e,t)=>{try{l.handshake_finish(t),i.authorized=!0,i._setupTlsStreams(l),i.alpnProtocol=i.getAlpnProtocol(),i._reading=!1,i._startReading(),i.emit(`secureConnect`)}catch(e){i.authorized=!1,i.authorizationError=e instanceof Error?e.message:String(e),c?i.destroy(e instanceof Error?e:Error(String(e))):(i._setupTlsStreams(l),i.emit(`secureConnect`))}})}catch(e){i.destroy(e instanceof Error?e:Error(String(e)))}}),i.connect({port:a,host:o}),i}function g(e){return{context:e||{}}}const _=[];function v(t,n){let r=Array.isArray(t)?t.map(e=>typeof e==`string`?e:e.toString(`utf-8`)).join(`
2
- `):typeof t==`string`?t:t.toString(`utf-8`),i=n?Array.isArray(n)?n.map(e=>typeof e==`string`?e:e.toString(`utf-8`)).join(`
3
- `):typeof n==`string`?n:n.toString(`utf-8`):``,a=i?`${r}\n${i}`:r;return e.TlsCertificate.new_from_pem(a,a.length)}var y=class extends n{_tlsCertificate=null;_tlsOptions;_sniContexts=new Map;constructor(e,t){if(super(),this._tlsOptions=e||{},t&&this.on(`secureConnection`,t),this._tlsOptions.cert)try{this._tlsCertificate=v(this._tlsOptions.cert,this._tlsOptions.key)}catch(e){a(this,`error`,i(e,`createServer`,{}))}}addContext(e,t){if(t.cert)try{let n=v(t.cert,t.key);this._sniContexts.set(e,n)}catch(e){this.emit(`error`,i(e,`addContext`,{}))}}listen(...e){return this.on(`connection`,e=>{this._upgradeTls(e)}),super.listen(...e)}_upgradeTls(n){let r=n._connection;if(!r){let e=Error(`Cannot upgrade socket: no underlying connection`);this.emit(`tlsClientError`,e,n),n.destroy();return}if(!this._tlsCertificate){let e=Error(`TLS server has no certificate configured`);this.emit(`tlsClientError`,e,n),n.destroy();return}try{let a=e.TlsServerConnection.new(r,this._tlsCertificate);if(this._tlsOptions.requestCert?a.authenticationMode=this._tlsOptions.rejectUnauthorized===!1?e.TlsAuthenticationMode.REQUESTED:e.TlsAuthenticationMode.REQUIRED:a.authenticationMode=e.TlsAuthenticationMode.NONE,this._tlsOptions.rejectUnauthorized===!1&&a.connect(`accept-certificate`,()=>!0),this._tlsOptions.ALPNProtocols&&this._tlsOptions.ALPNProtocols.length>0)try{a.set_advertised_protocols(this._tlsOptions.ALPNProtocols)}catch{}let o=new e.Cancellable;a.handshake_async(t.PRIORITY_DEFAULT,o,(e,t)=>{try{a.handshake_finish(t);let e=new m;e.encrypted=!0,e.authorized=!0,e._setupTlsStreams(a),e.alpnProtocol=e.getAlpnProtocol(),e._startReading(),this.emit(`secureConnection`,e)}catch(e){let t=i(e,`handshake`,{});this.emit(`tlsClientError`,t,n),n.destroy()}})}catch(e){let t=i(e,`tls_wrap`,{});this.emit(`tlsClientError`,t,n),n.destroy()}}};function b(e,t){return typeof e==`function`?new y(void 0,e):new y(e,t)}var x={TLSSocket:m,TLSServer:y,Server:y,connect:h,createServer:b,createSecureContext:g,checkServerIdentity:p,getCiphers:l,rootCertificates:_,DEFAULT_MIN_VERSION:o,DEFAULT_MAX_VERSION:s,DEFAULT_CIPHERS:c};export{c as DEFAULT_CIPHERS,s as DEFAULT_MAX_VERSION,o as DEFAULT_MIN_VERSION,y as Server,y as TLSServer,m as TLSSocket,p as checkServerIdentity,h as connect,g as createSecureContext,b as createServer,x as default,l as getCiphers,_ as rootCertificates};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gio-2.0";import t from"@girs/glib-2.0";import{Server as n,Socket as r}from"node:net";import{createNodeError as i,deferEmit as a}from"@gjsify/utils";const o=`TLSv1.2`,s=`TLSv1.3`,c=`TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384`;function getCiphers(){return[`aes-128-gcm`,`aes-256-gcm`,`chacha20-poly1305`,`aes-128-cbc`,`aes-256-cbc`]}function pemToString(e){if(Array.isArray(e))return e.map(pemToString).join(`
2
+ `);if(typeof e==`string`)return e;if(e&&typeof e.toString==`function`)try{return e.toString(`utf-8`)}catch{return new TextDecoder(`utf-8`).decode(e)}return String(e)}function splitPemBlocks(e){let t=[],n=/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g,r;for(;(r=n.exec(e))!==null;)t.push(r[0]);return t}function buildGioCertificate(t,n){let r=pemToString(t),i=n?pemToString(n):``,a=i?`${r}\n${i}`:r;return e.TlsCertificate.new_from_pem(a,a.length)}function buildCaCertificates(t){let n=[];if(Array.isArray(t))for(let e of t)n.push(...splitPemBlocks(pemToString(e)));else n.push(...splitPemBlocks(pemToString(t)));let r=[];for(let t of n)try{r.push(e.TlsCertificate.new_from_pem(t,t.length))}catch{}return r}function parseDistinguishedName(e){if(!e)return{};let t={};for(let n of e.split(/,(?![^=]*=)/)){let e=n.indexOf(`=`);if(e<0)continue;let r=n.slice(0,e).trim(),i=n.slice(e+1).trim(),a=t[r];a===void 0?t[r]=i:Array.isArray(a)?a.push(i):t[r]=[a,i]}return t}function formatCertDate(e){if(!e)return``;try{return e.format(`%b %d %H:%M:%S %Y GMT`)??``}catch{return``}}function formatAltNames(e){let t=[];try{let n=e.get_dns_names();if(n)for(let e of n){let n=e.get_data();n&&t.push(`DNS:${new TextDecoder(`utf-8`).decode(n)}`)}}catch{}try{let n=e.get_ip_addresses();if(n)for(let e of n)t.push(`IP Address:${e.to_string()}`)}catch{}return t.join(`, `)}function fingerprintFromBytes(e,n){try{let r=new t.Checksum(n);r.update(e);let i=r.get_string();if(!i)return``;let a=[];for(let e=0;e<i.length;e+=2)a.push(i.slice(e,e+2).toUpperCase());return a.join(`:`)}catch{return``}}function pemToDer(e){let t=/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/.exec(e);if(!t)return new Uint8Array;let n=t[1].replace(/[\s\r\n]+/g,``);try{let e=globalThis.atob;if(!e)return new Uint8Array;let t=e(n),r=new Uint8Array(t.length);for(let e=0;e<t.length;e++)r[e]=t.charCodeAt(e);return r}catch{return new Uint8Array}}function tlsCertToPeerCert(e,n){let r={};try{r.subject=parseDistinguishedName(e.get_subject_name())}catch{}try{r.issuer=parseDistinguishedName(e.get_issuer_name())}catch{}r.subjectaltname=formatAltNames(e);try{r.valid_from=formatCertDate(e.get_not_valid_before()),r.valid_to=formatCertDate(e.get_not_valid_after())}catch{}try{let n=e,i=n.certificate_pem??n.certificatePem;if(i){let e=pemToDer(i);r.raw=e,r.fingerprint=fingerprintFromBytes(e,t.ChecksumType.SHA1),r.fingerprint256=fingerprintFromBytes(e,t.ChecksumType.SHA256)}}catch{}if(n)try{let t=e.get_issuer();t&&!t.is_same(e)?r.issuerCertificate=tlsCertToPeerCert(t,!0):t&&(r.issuerCertificate=r)}catch{}return r}function unfqdn(e){return e.endsWith(`.`)?e.slice(0,-1):e}function splitHost(e){return unfqdn(e).toLowerCase().split(`.`)}function isPrintableAscii(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<33||n>126)return!1}return!0}function checkHostMatch(e,t){if(!t)return!1;let n=splitHost(t);if(e.length!==n.length||n.includes(``)||!n.every(isPrintableAscii))return!1;for(let t=e.length-1;t>0;t--)if(e[t]!==n[t])return!1;let r=e[0],i=n[0],a=i.split(`*`,3);if(a.length===1||i.includes(`xn--`))return r===i;if(a.length>2||n.length<=2)return!1;let o=a[0],s=a[1];return!(o.length+s.length>r.length||!r.startsWith(o)||!r.endsWith(s))}function checkServerIdentity(e,t){let n=t.subject,r=t.subjectaltname,i=[],a=[];if(e=String(e),r){let e=r.split(`, `);for(let t of e)t.startsWith(`DNS:`)?i.push(t.slice(4)):t.startsWith(`IP Address:`)&&a.push(t.slice(11).trim())}let o=!1,s=`Unknown reason`;e=unfqdn(e);let c=/^(\d{1,3}\.){3}\d{1,3}$/.test(e),l=e.includes(`:`);if(c||l)o=a.some(t=>t.toLowerCase()===e.toLowerCase()),o||(s=`IP: ${e} is not in the cert's list: ${a.join(`, `)}`);else if(i.length>0||n?.CN){let t=splitHost(e);if(i.length>0)o=i.some(e=>checkHostMatch(t,e.trim())),o||(s=`Host: ${e}. is not in the cert's altnames: ${r}`);else{let r=n?.CN;Array.isArray(r)?o=r.some(e=>checkHostMatch(t,e)):r&&(o=checkHostMatch(t,r)),o||(s=`Host: ${e}. is not cert's CN: ${r}`)}}else s=`Cert does not contain a DNS name`;if(!o){let n=Error(s);return n.reason=s,n.host=e,n.cert=t,n.code=`ERR_TLS_CERT_ALTNAME_INVALID`,n}}function createSecureContext(e){let t=e??{},n=null;if(t.cert)try{n=buildGioCertificate(t.cert,t.key)}catch{n=null}let r=t.ca?buildCaCertificates(t.ca):[],i={certificate:n,caCertificates:r,options:t};return i.context=i,i}var TLSSocket=class extends r{encrypted=!0;authorized=!1;authorizationError;alpnProtocol=!1;servername;_tlsConnection=null;_secureContext=null;constructor(e,t){super()}_setupTlsStreams(e){this._tlsConnection=e;let t=this;t._inputStream=e.get_input_stream(),t._outputStream=e.get_output_stream(),t._connection=e}getPeerCertificate(e=!1){if(!this._tlsConnection)return{};try{let t=this._tlsConnection.get_peer_certificate();return t?tlsCertToPeerCert(t,e):{}}catch{return{}}}getProtocol(){if(!this._tlsConnection)return null;try{switch(this._tlsConnection.get_protocol_version()){case e.TlsProtocolVersion.TLS_1_0:return`TLSv1`;case e.TlsProtocolVersion.TLS_1_1:return`TLSv1.1`;case e.TlsProtocolVersion.TLS_1_2:return`TLSv1.2`;case e.TlsProtocolVersion.TLS_1_3:return`TLSv1.3`;default:return null}}catch{return null}}getCipher(){if(!this._tlsConnection)return null;try{return{name:this._tlsConnection.get_ciphersuite_name()||`unknown`,version:this.getProtocol()||`unknown`}}catch{return null}}getAlpnProtocol(){if(!this._tlsConnection)return!1;try{return this._tlsConnection.get_negotiated_protocol()||!1}catch{return!1}}};function connect(n,r){let i=new TLSSocket(void 0,n);r&&i.once(`secureConnect`,r);let a=n.port||443,o=n.host||`localhost`,s=n.servername||o,c=n.rejectUnauthorized!==!1,l=n.secureContext??createSecureContext(n);i._secureContext=l,i.servername=s;let u=n.checkServerIdentity;return i.once(`connect`,()=>{let r=i._connection;if(!r){i.destroy(Error(`No underlying connection for TLS upgrade`));return}try{let o=e.NetworkAddress.new(s,a),d=e.TlsClientConnection.new(r,o);if(d.set_server_identity(o),l.certificate)try{d.set_certificate(l.certificate)}catch(e){console.warn(`[tls] failed to set client certificate:`,e)}if(n.ALPNProtocols&&n.ALPNProtocols.length>0)try{d.set_advertised_protocols(n.ALPNProtocols)}catch{}d.connect(`accept-certificate`,(t,n,r)=>{if(!c)return!0;if(l.caCertificates.length===0)return!1;for(let t of l.caCertificates)try{if(n.verify(o,t)===e.TlsCertificateFlags.NO_FLAGS)return!0}catch{}return!1});let f=new e.Cancellable;d.handshake_async(t.PRIORITY_DEFAULT,f,(e,t)=>{try{if(d.handshake_finish(t),i.authorized=!0,i._setupTlsStreams(d),i.alpnProtocol=i.getAlpnProtocol(),u){let e=u(s,i.getPeerCertificate());if(e&&(i.authorized=!1,i.authorizationError=e.message,c)){i.destroy(e);return}}let e=i;e._reading=!1,e._startReading(),i.emit(`secureConnect`)}catch(e){i.authorized=!1,i.authorizationError=e instanceof Error?e.message:String(e),c?i.destroy(e instanceof Error?e:Error(String(e))):(i._setupTlsStreams(d),i.emit(`secureConnect`))}})}catch(e){i.destroy(e instanceof Error?e:Error(String(e)))}}),i.connect({port:a,host:o}),i}const l=[];var TLSServer=class extends n{_tlsCertificate=null;_tlsOptions;_sniContexts=new Map;_secureContext;constructor(e,t){super(),this._tlsOptions=e??{},this._secureContext=createSecureContext(this._tlsOptions),this._tlsCertificate=this._secureContext.certificate,t&&this.on(`secureConnection`,t),this._tlsOptions.cert&&!this._tlsCertificate&&a(this,`error`,i(Error(`Failed to parse TLS certificate`),`createServer`,{}))}addContext(e,t){try{let n=createSecureContext(t);this._sniContexts.set(e.toLowerCase(),n)}catch(e){this.emit(`error`,i(e,`addContext`,{}))}}_resolveSniContext(e,t){let n=this._secureContext;if(!e){t(n);return}let r=e.toLowerCase(),i=this._sniContexts.get(r);if(i){t(i);return}let a=splitHost(r);for(let[e,n]of this._sniContexts)if(checkHostMatch(a,e)){t(n);return}if(this._tlsOptions.SNICallback)try{this._tlsOptions.SNICallback(e,(e,r)=>{if(e||!r){t(n);return}t(r)});return}catch{t(n);return}t(n)}listen(...e){return this.on(`connection`,e=>{this._upgradeTls(e)}),super.listen(...e)}_upgradeTls(n){let r=n._connection;if(!r){let e=Error(`Cannot upgrade socket: no underlying connection`);this.emit(`tlsClientError`,e,n),n.destroy();return}if(!this._tlsCertificate&&this._sniContexts.size===0&&!this._tlsOptions.SNICallback){let e=Error(`TLS server has no certificate configured`);this.emit(`tlsClientError`,e,n),n.destroy();return}this._resolveSniContext(null,a=>{let o=a.certificate??this._tlsCertificate;if(!o){let e=Error(`SNI resolution returned no certificate`);this.emit(`tlsClientError`,e,n),n.destroy();return}try{let s=e.TlsServerConnection.new(r,o);this._tlsOptions.requestCert?s.authenticationMode=this._tlsOptions.rejectUnauthorized===!1?e.TlsAuthenticationMode.REQUESTED:e.TlsAuthenticationMode.REQUIRED:s.authenticationMode=e.TlsAuthenticationMode.NONE;let c=!!this._tlsOptions.requestCert&&this._tlsOptions.rejectUnauthorized!==!1,l=this._secureContext.caCertificates;if(s.connect(`accept-certificate`,(t,n,r)=>{if(!c)return!0;if(l.length===0)return!1;for(let t of l)try{if(n.verify(null,t)===e.TlsCertificateFlags.NO_FLAGS)return!0}catch{}return!1}),this._tlsOptions.ALPNProtocols&&this._tlsOptions.ALPNProtocols.length>0)try{s.set_advertised_protocols(this._tlsOptions.ALPNProtocols)}catch{}let u=new e.Cancellable;s.handshake_async(t.PRIORITY_DEFAULT,u,(e,t)=>{try{s.handshake_finish(t);let e=new TLSSocket;e.encrypted=!0,e.authorized=!0,e._secureContext=a,e._setupTlsStreams(s),e.alpnProtocol=e.getAlpnProtocol(),e._startReading(),this.emit(`secureConnection`,e)}catch(e){let t=i(e,`handshake`,{});this.emit(`tlsClientError`,t,n),n.destroy()}})}catch(e){let t=i(e,`tls_wrap`,{});this.emit(`tlsClientError`,t,n),n.destroy()}})}};function createServer(e,t){return typeof e==`function`?new TLSServer(void 0,e):new TLSServer(e,t)}const u={TLSSocket,TLSServer,Server:TLSServer,connect,createServer,createSecureContext,checkServerIdentity,getCiphers,rootCertificates:l,DEFAULT_MIN_VERSION:o,DEFAULT_MAX_VERSION:s,DEFAULT_CIPHERS:c};export{c as DEFAULT_CIPHERS,s as DEFAULT_MAX_VERSION,o as DEFAULT_MIN_VERSION,TLSServer as Server,TLSServer,TLSSocket,checkServerIdentity,connect,createSecureContext,createServer,u as default,getCiphers,l as rootCertificates};
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -5,33 +5,74 @@ export declare const DEFAULT_MAX_VERSION = "TLSv1.3";
5
5
  export declare const DEFAULT_CIPHERS = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384";
6
6
  /** Returns a list of supported TLS cipher names (subset; implementation-defined). */
7
7
  export declare function getCiphers(): string[];
8
+ type PemInput = string | Buffer | Uint8Array | Array<string | Buffer | Uint8Array>;
9
+ export interface CertSubject {
10
+ CN?: string | string[];
11
+ [key: string]: unknown;
12
+ }
8
13
  export interface PeerCertificate {
9
- subject?: {
10
- CN?: string | string[];
11
- [key: string]: unknown;
12
- };
14
+ subject?: CertSubject;
15
+ issuer?: CertSubject;
13
16
  subjectaltname?: string;
17
+ valid_from?: string;
18
+ valid_to?: string;
19
+ fingerprint?: string;
20
+ fingerprint256?: string;
21
+ serialNumber?: string;
22
+ raw?: Uint8Array;
23
+ issuerCertificate?: PeerCertificate;
14
24
  [key: string]: unknown;
15
25
  }
26
+ /** Error returned by checkServerIdentity, with Node-compatible shape. */
27
+ export interface CertAltNameError extends Error {
28
+ reason: string;
29
+ host: string;
30
+ cert: PeerCertificate;
31
+ code: 'ERR_TLS_CERT_ALTNAME_INVALID';
32
+ }
16
33
  /**
17
34
  * Verifies that the certificate `cert` is valid for `hostname`.
18
- * Returns an Error if the check fails, or undefined on success.
35
+ * Returns an Error (with code 'ERR_TLS_CERT_ALTNAME_INVALID') if the check
36
+ * fails, or `undefined` on success.
19
37
  *
20
- * Reference: Node.js lib/tls.js exports.checkServerIdentity
38
+ * Reference: Node.js lib/tls.js exports.checkServerIdentity (RFC 6125 §6.4.3).
21
39
  */
22
- export declare function checkServerIdentity(hostname: string, cert: PeerCertificate): Error | undefined;
40
+ export declare function checkServerIdentity(hostname: string, cert: PeerCertificate): CertAltNameError | undefined;
23
41
  export interface SecureContextOptions {
24
- ca?: string | Buffer | Array<string | Buffer>;
25
- cert?: string | Buffer | Array<string | Buffer>;
26
- key?: string | Buffer | Array<string | Buffer>;
42
+ ca?: PemInput;
43
+ cert?: PemInput;
44
+ key?: PemInput;
45
+ passphrase?: string;
27
46
  rejectUnauthorized?: boolean;
47
+ ciphers?: string;
48
+ minVersion?: string;
49
+ maxVersion?: string;
50
+ ALPNProtocols?: string[];
51
+ }
52
+ /** Internal "secure context" — parsed TLS material shared by tls.connect/createServer. */
53
+ export interface SecureContext {
54
+ certificate: Gio.TlsCertificate | null;
55
+ caCertificates: Gio.TlsCertificate[];
56
+ options: SecureContextOptions;
57
+ /**
58
+ * Node-compat handle (Node returns a `SecureContext` with an internal native
59
+ * `context` field). We have no native handle, so this points back at the
60
+ * SecureContext object itself — `ctx.context !== undefined` matches Node.
61
+ */
62
+ context: SecureContext;
28
63
  }
64
+ /** Build a SecureContext from PEM material. Buffer/Uint8Array/string all accepted. */
65
+ export declare function createSecureContext(options?: SecureContextOptions): SecureContext;
29
66
  export interface TlsConnectOptions extends SecureContextOptions {
30
67
  host?: string;
31
68
  port?: number;
32
69
  socket?: Socket;
33
70
  servername?: string;
34
71
  ALPNProtocols?: string[];
72
+ /** Pre-built secure context from createSecureContext(). */
73
+ secureContext?: SecureContext;
74
+ /** Custom server-identity check (runs after the GnuTLS-level check). */
75
+ checkServerIdentity?: (host: string, cert: PeerCertificate) => Error | undefined;
35
76
  }
36
77
  /**
37
78
  * TLSSocket wraps a net.Socket with TLS via Gio.TlsConnection.
@@ -41,24 +82,31 @@ export declare class TLSSocket extends Socket {
41
82
  authorized: boolean;
42
83
  authorizationError?: string;
43
84
  alpnProtocol: string | false;
85
+ servername: string | undefined;
44
86
  /** @internal */
45
87
  _tlsConnection: Gio.TlsConnection | null;
46
- constructor(socket?: Socket, options?: SecureContextOptions);
88
+ /** @internal preserved for diagnostics + future cert-chain verification. */
89
+ _secureContext: SecureContext | null;
90
+ constructor(_socket?: Socket, _options?: SecureContextOptions);
47
91
  /**
48
92
  * @internal Wire the TLS connection's I/O streams into this socket
49
93
  * so that read/write operations go through the encrypted channel.
50
94
  */
51
95
  _setupTlsStreams(tlsConn: Gio.TlsConnection): void;
52
- /** Get the peer certificate info. */
53
- getPeerCertificate(_detailed?: boolean): any;
96
+ /**
97
+ * Get the peer certificate. When `detailed` is true, walks the issuer chain
98
+ * via `Gio.TlsCertificate.get_issuer()` and populates `issuerCertificate`
99
+ * recursively (with a self-reference on the root for compatibility).
100
+ */
101
+ getPeerCertificate(detailed?: boolean): PeerCertificate;
54
102
  /** Get the negotiated TLS protocol version. */
55
103
  getProtocol(): string | null;
56
- /** Get the cipher info. */
104
+ /** Get the negotiated cipher suite name + version. */
57
105
  getCipher(): {
58
106
  name: string;
59
107
  version: string;
60
108
  } | null;
61
- /** Get the negotiated ALPN protocol. */
109
+ /** Get the negotiated ALPN protocol (or false if none). */
62
110
  getAlpnProtocol(): string | false;
63
111
  }
64
112
  /**
@@ -68,34 +116,41 @@ export declare class TLSSocket extends Socket {
68
116
  * the connection to TLS using Gio.TlsClientConnection.
69
117
  */
70
118
  export declare function connect(options: TlsConnectOptions, callback?: () => void): TLSSocket;
71
- /**
72
- * Create a TLS secure context.
73
- */
74
- export declare function createSecureContext(options?: SecureContextOptions): {
75
- context: any;
76
- };
77
119
  export declare const rootCertificates: string[];
120
+ export type SNICallback = (servername: string, cb: (err: Error | null, ctx?: SecureContext) => void) => void;
78
121
  export interface TlsServerOptions extends SecureContextOptions {
79
122
  requestCert?: boolean;
80
123
  rejectUnauthorized?: boolean;
81
124
  ALPNProtocols?: string[];
125
+ SNICallback?: SNICallback;
82
126
  }
83
127
  /**
84
- * TLSServer wraps a net.Server to accept TLS connections.
128
+ * TLSServer accepts incoming TCP connections and upgrades each to TLS via
129
+ * `Gio.TlsServerConnection`. Supports mTLS via `requestCert`+`rejectUnauthorized`,
130
+ * SNI selection via `addContext`/`SNICallback`, and ALPN negotiation.
85
131
  */
86
132
  export declare class TLSServer extends Server {
87
133
  private _tlsCertificate;
88
134
  private _tlsOptions;
89
135
  private _sniContexts;
136
+ /** @internal — exposed for tests. */
137
+ _secureContext: SecureContext;
90
138
  constructor(options?: TlsServerOptions, secureConnectionListener?: (socket: TLSSocket) => void);
91
139
  /**
92
- * Add a context for SNI (Server Name Indication).
140
+ * Add an additional context for SNI (Server Name Indication). Uses RFC 6125
141
+ * matching against the requested server name.
93
142
  */
94
143
  addContext(hostname: string, context: SecureContextOptions): void;
95
- listen(...args: unknown[]): this;
96
144
  /**
97
- * Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection.
145
+ * Resolve a SecureContext for the given server name. Order:
146
+ * 1. exact match in `_sniContexts`
147
+ * 2. RFC 6125 wildcard match in `_sniContexts`
148
+ * 3. SNICallback (if provided)
149
+ * 4. fall through to the server's default context
98
150
  */
151
+ private _resolveSniContext;
152
+ listen(...args: unknown[]): this;
153
+ /** Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection. */
99
154
  private _upgradeTls;
100
155
  }
101
156
  /**
@@ -104,7 +159,7 @@ export declare class TLSServer extends Server {
104
159
  export declare function createServer(options?: TlsServerOptions, secureConnectionListener?: (socket: TLSSocket) => void): TLSServer;
105
160
  export declare function createServer(secureConnectionListener?: (socket: TLSSocket) => void): TLSServer;
106
161
  export { TLSServer as Server };
107
- declare const _default: {
162
+ declare const tlsExports: {
108
163
  TLSSocket: typeof TLSSocket;
109
164
  TLSServer: typeof TLSServer;
110
165
  Server: typeof TLSServer;
@@ -118,4 +173,4 @@ declare const _default: {
118
173
  DEFAULT_MAX_VERSION: string;
119
174
  DEFAULT_CIPHERS: string;
120
175
  };
121
- export default _default;
176
+ export default tlsExports;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/tls",
3
- "version": "0.3.20",
3
+ "version": "0.4.0",
4
4
  "description": "Node.js tls module for Gjs",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -30,15 +30,15 @@
30
30
  "tls"
31
31
  ],
32
32
  "devDependencies": {
33
- "@gjsify/cli": "^0.3.20",
34
- "@gjsify/unit": "^0.3.20",
33
+ "@gjsify/cli": "^0.4.0",
34
+ "@gjsify/unit": "^0.4.0",
35
35
  "@types/node": "^25.6.2",
36
36
  "typescript": "^6.0.3"
37
37
  },
38
38
  "dependencies": {
39
- "@girs/gio-2.0": "2.88.0-4.0.0-rc.14",
40
- "@girs/glib-2.0": "2.88.0-4.0.0-rc.14",
41
- "@gjsify/net": "^0.3.20",
42
- "@gjsify/utils": "^0.3.20"
39
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.15",
40
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
41
+ "@gjsify/net": "^0.4.0",
42
+ "@gjsify/utils": "^0.4.0"
43
43
  }
44
44
  }
@@ -0,0 +1,230 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Ported from refs/node-test/parallel/test-tls-check-server-identity.js,
3
+ // refs/node-test/parallel/test-tls-canonical-ip.js,
4
+ // refs/node-test/parallel/test-tls-checkserveridentity-no-altnames.js.
5
+ // Original: Copyright (c) Node.js contributors. MIT.
6
+ // Rewritten for @gjsify/unit — behavior preserved, assertion dialect adapted.
7
+ //
8
+ // Focused coverage of RFC 6125 (§6.4.3) hostname matching + getPeerCertificate
9
+ // shape + createSecureContext PEM acceptance. Complements index.spec.ts.
10
+
11
+ import { describe, it, expect, on } from '@gjsify/unit';
12
+ import {
13
+ checkServerIdentity,
14
+ createSecureContext,
15
+ } from 'node:tls';
16
+ import type { PeerCertificate } from 'node:tls';
17
+
18
+ function fakeCert(parts: Record<string, unknown>): PeerCertificate {
19
+ return parts as unknown as PeerCertificate;
20
+ }
21
+
22
+ // Self-signed PEM (cert + key) minted with `openssl req -x509 -newkey rsa:2048
23
+ // -keyout key.pem -out cert.pem -days 3650 -nodes -subj /CN=test.example`.
24
+ // 2048-bit RSA, SHA256, no SAN. Used only for parser smoke-tests — never
25
+ // trusted as a CA. Inlined so tests stay hermetic (no fixtures).
26
+ const SAMPLE_CERT_PEM = `-----BEGIN CERTIFICATE-----
27
+ MIIDDzCCAfegAwIBAgIUO9v7luuPxFHsLdVr//dOTh474OQwDQYJKoZIhvcNAQEL
28
+ BQAwFzEVMBMGA1UEAwwMdGVzdC5leGFtcGxlMB4XDTI2MDUwOTA2NDIyMVoXDTM2
29
+ MDUwNjA2NDIyMVowFzEVMBMGA1UEAwwMdGVzdC5leGFtcGxlMIIBIjANBgkqhkiG
30
+ 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtnMdp3rW+bkeYoW9at2aRt70gub5siMsNOYw
31
+ 0obCo1LOkJokEOX2+9kaOUwaCmntcJdU3+JYQe7MGYwI+H9OWxnLRYJUneYB81a8
32
+ CfJ7Uk+5FVNzQQFnYvL0G5NSq2w8K6UwV0jgXXFLEfTZOqdFdVVcwAOgCrc4LbpX
33
+ E+5IflPoIuwBW2brYx/fIyJu2gH0uRG92R5m/3UxOqlVzIerL3YM3UTSepDtyCek
34
+ ooD03UwLl3fYuza2iuMnxJAPshaSwiHbAopnaBSEvKMGHkLyl4zTmaioGXVRObtZ
35
+ XAorC7ujI4F8edcmPRwAaNToHS8cRlGQJjXACfAodwNKWpbHYwIDAQABo1MwUTAd
36
+ BgNVHQ4EFgQULhAoOyRjZxBRF8FlwTySSAXnFo0wHwYDVR0jBBgwFoAULhAoOyRj
37
+ ZxBRF8FlwTySSAXnFo0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
38
+ AQEAiePCYXf6PAF/m3SE9PYNLiq+ALeJ+kQgXUYp5S4vsfk2T+XmbtP0uNnTQpsT
39
+ 4GaytjMFIQiNO6mHww8SFnrEr9btcNGCg17gyK7T3XukbsXIgOYvYjJboxb6Ww1T
40
+ YFEXqPv55of/I5+UPH88WOYR1JsPE6lR4s3MfaMMRXTIZwpob2sxEfqHfti1DrHV
41
+ nmzZpIvZi3II+gpx6aYE8m3DjgPrxbXw78Ular/VyYAyy9XVFpcl2nrAQgpErZIr
42
+ kweGVVMKE8qMF9SO1/pER4T7A9ZBJol00hMCL+5Rw0Pw4lCLXgGX1J9VyZ+ScLxz
43
+ aNy5t6tyGuGk0sol4dLG8nzHGA==
44
+ -----END CERTIFICATE-----`;
45
+
46
+ const SAMPLE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
47
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2cx2netb5uR5i
48
+ hb1q3ZpG3vSC5vmyIyw05jDShsKjUs6QmiQQ5fb72Ro5TBoKae1wl1Tf4lhB7swZ
49
+ jAj4f05bGctFglSd5gHzVrwJ8ntST7kVU3NBAWdi8vQbk1KrbDwrpTBXSOBdcUsR
50
+ 9Nk6p0V1VVzAA6AKtzgtulcT7kh+U+gi7AFbZutjH98jIm7aAfS5Eb3ZHmb/dTE6
51
+ qVXMh6svdgzdRNJ6kO3IJ6SigPTdTAuXd9i7NraK4yfEkA+yFpLCIdsCimdoFIS8
52
+ owYeQvKXjNOZqKgZdVE5u1lcCisLu6MjgXx51yY9HABo1OgdLxxGUZAmNcAJ8Ch3
53
+ A0palsdjAgMBAAECggEADTZAcBYmjSAWDpjHguT+cAiWXwhU2pm1CmhrVRBD9fz5
54
+ tZT3IAlXHYO2/dRmw/KwKnAUlr9wS4EvlhLPvEotWJsa+JlogT3cgRktz4nLDmBB
55
+ jRH+9AOJaUWIq2dVHDhfq+I8oh8TFREumEoWLpFZ9Hya+9I6lUWq5smdcEI+gHWs
56
+ yWhq4auR2/zkhvgsjLKPK8P2M8tmm8IPecE74NSBZqxoqb4upUHpMTxasAE2UJpK
57
+ 0slvxiZtk3LT/rflco04twIqK/KmH7phrPzmoSl+Lp1Zh9y4iNznTQKBdImsGl+g
58
+ 9FWL7pdxKjVNGQsrKVvB1A78VhhteAuaebT5MicIwQKBgQDtmYfyrlGoYGkArmNL
59
+ tGBc5afGx/mL/8BGTQfIx4b11OEr+G4hppLh/WGLUksfMfprL0YU3BM+FOsECsY0
60
+ 5AJB6oQuSE8vCIS7yCxV00V+I6n2bEMxZkDwdDuF9rHhGODFx4pPz1nJqScAa23T
61
+ 48c+hNSBLRHTSuDAi2DcuxLn4QKBgQDElDjmB/kpW50ya2M2hO5LpwMcAlTJ3nPv
62
+ 000eTfzdtDNzesvRClYZWGWs7zSkNsf/F+G0tCg4slzvHPkRJN53hHF0Kwug2aj5
63
+ 1BPuDbn3EumDO6quykz6S9vZGcPAMuNm+qtkf+42KE5Sqj3NTf6aiu4tEZbH69me
64
+ av1sbfkHwwKBgQC5cb5g1FuZjn4F8RZBDSy09O4pQQVtlpSsigzMUabtklSY7BKR
65
+ IyC7T/dlNTq6w1hPdhs9xrMiHlN72SjwORHl/rNiKD/dVsm6grbP2dEAbbeHROKA
66
+ 2O1Qf3fBzFTzemZdF6vFNPJAakytkCutWLe2/RebJuElx+h5f49/WGeeIQKBgQC0
67
+ mcCUharB9ms7kTF7OzF6y5utte6T8A3vvd9SAjBYt1+1rpFmIersKixvbuycGcAw
68
+ eo5gaEuzmxqKi8G/oHHKuCFLquhqBM6bh94vjOjXN8bVTJIJN870/ZCjqmoPQDFv
69
+ wMiJ8oa1tt4OUF2rKwbIkO809L3kOqiaRI1Det2Z5QKBgGp4JS3OqzeIMQbUa8e8
70
+ tOYnML+RFpo2cZ9rCQAQitm/w5P6lAnG9vv4cXAu94RbtITEuGmqEhYDMhSC2i8e
71
+ kjwRLp5R+/pynUdTckRP8buwRSDRlPz3x8GI2Dm0z8fb/uZ8AJK/iITtsOoJtyPx
72
+ fUZ521uRyKXnpzj1HTz5WVp0
73
+ -----END PRIVATE KEY-----`;
74
+
75
+ export default async () => {
76
+ await describe('tls — RFC 6125 + cert extraction', async () => {
77
+
78
+ // ---------------- RFC 6125 wildcard depth rules ----------------
79
+ await describe('RFC 6125 wildcard depth', async () => {
80
+ await it('rejects two-label wildcard *.com', async () => {
81
+ const err = checkServerIdentity('foo.com', fakeCert({
82
+ subject: {},
83
+ subjectaltname: 'DNS:*.com',
84
+ }));
85
+ expect(err instanceof Error).toBe(true);
86
+ });
87
+
88
+ await it('rejects single-label wildcard *', async () => {
89
+ const err = checkServerIdentity('foo', fakeCert({
90
+ subject: {},
91
+ subjectaltname: 'DNS:*',
92
+ }));
93
+ expect(err instanceof Error).toBe(true);
94
+ });
95
+
96
+ await it('accepts three-label wildcard *.example.com matching foo.example.com', async () => {
97
+ const ok = checkServerIdentity('foo.example.com', fakeCert({
98
+ subject: {},
99
+ subjectaltname: 'DNS:*.example.com',
100
+ }));
101
+ expect(ok).toBeUndefined();
102
+ });
103
+
104
+ await it('rejects empty wildcard label .x.example.com', async () => {
105
+ const err = checkServerIdentity('foo.x.example.com', fakeCert({
106
+ subject: {},
107
+ subjectaltname: 'DNS:.x.example.com',
108
+ }));
109
+ expect(err instanceof Error).toBe(true);
110
+ });
111
+ });
112
+
113
+ // ---------------- RFC 6125 wildcard prefix/suffix ----------------
114
+ await describe('RFC 6125 wildcard prefix/suffix', async () => {
115
+ await it('matches f*.example.com against foo.example.com', async () => {
116
+ const ok = checkServerIdentity('foo.example.com', fakeCert({
117
+ subject: {},
118
+ subjectaltname: 'DNS:f*.example.com',
119
+ }));
120
+ expect(ok).toBeUndefined();
121
+ });
122
+
123
+ await it('rejects f*.example.com against bar.example.com', async () => {
124
+ const err = checkServerIdentity('bar.example.com', fakeCert({
125
+ subject: {},
126
+ subjectaltname: 'DNS:f*.example.com',
127
+ }));
128
+ expect(err instanceof Error).toBe(true);
129
+ });
130
+
131
+ await it('matches *foo.example.com against barfoo.example.com', async () => {
132
+ const ok = checkServerIdentity('barfoo.example.com', fakeCert({
133
+ subject: {},
134
+ subjectaltname: 'DNS:*foo.example.com',
135
+ }));
136
+ expect(ok).toBeUndefined();
137
+ });
138
+
139
+ await it('rejects f*r.example.com against bar.example.com (prefix mismatch)', async () => {
140
+ const err = checkServerIdentity('bar.example.com', fakeCert({
141
+ subject: {},
142
+ subjectaltname: 'DNS:f*r.example.com',
143
+ }));
144
+ expect(err instanceof Error).toBe(true);
145
+ });
146
+ });
147
+
148
+ // ---------------- IDN / xn-- handling ----------------
149
+ await describe('Punycode / IDN', async () => {
150
+ await it('treats xn-- labels as exact-match (no wildcard expansion)', async () => {
151
+ // pattern is the punycoded form; hostname must match exactly.
152
+ const ok = checkServerIdentity('xn--bcher-kva.example.com', fakeCert({
153
+ subject: {},
154
+ subjectaltname: 'DNS:xn--bcher-kva.example.com',
155
+ }));
156
+ expect(ok).toBeUndefined();
157
+ });
158
+
159
+ await it('rejects wildcard expansion against xn-- A-label leftmost', async () => {
160
+ // *xn--bcher-kva matched against bcher-kva: per RFC 6125 the wildcard
161
+ // is not allowed to expand into A-label content.
162
+ const err = checkServerIdentity('foo.example.com', fakeCert({
163
+ subject: {},
164
+ subjectaltname: 'DNS:xn--*.example.com',
165
+ }));
166
+ expect(err instanceof Error).toBe(true);
167
+ });
168
+ });
169
+
170
+ // ---------------- Error code shape (Node-compat) ----------------
171
+ await describe('error.code', async () => {
172
+ await it('returns ERR_TLS_CERT_ALTNAME_INVALID code on mismatch', async () => {
173
+ const err = checkServerIdentity('a.com', fakeCert({
174
+ subject: { CN: 'b.com' },
175
+ }));
176
+ expect(err instanceof Error).toBe(true);
177
+ const e = err as { code?: string };
178
+ // Node: ERR_TLS_CERT_ALTNAME_INVALID. Our impl: same.
179
+ expect(e.code).toBe('ERR_TLS_CERT_ALTNAME_INVALID');
180
+ });
181
+ });
182
+
183
+ // ---------------- createSecureContext PEM acceptance ----------------
184
+ await describe('createSecureContext', async () => {
185
+ await it('accepts string PEM material (cert + key)', async () => {
186
+ const ctx = createSecureContext({ cert: SAMPLE_CERT_PEM, key: SAMPLE_KEY_PEM });
187
+ expect(ctx).toBeDefined();
188
+ });
189
+
190
+ await it('accepts Buffer PEM material', async () => {
191
+ const Buffer = (globalThis as { Buffer?: { from(s: string): unknown } }).Buffer;
192
+ if (!Buffer) return; // skip if Buffer unavailable
193
+ const ctx = createSecureContext({
194
+ cert: Buffer.from(SAMPLE_CERT_PEM) as never,
195
+ key: Buffer.from(SAMPLE_KEY_PEM) as never,
196
+ });
197
+ expect(ctx).toBeDefined();
198
+ });
199
+
200
+ await it('accepts array of PEM blocks for ca', async () => {
201
+ const ctx = createSecureContext({ ca: [SAMPLE_CERT_PEM, SAMPLE_CERT_PEM] });
202
+ expect(ctx).toBeDefined();
203
+ });
204
+
205
+ await it('returns ctx.context self-reference (Node-compat)', async () => {
206
+ const ctx = createSecureContext();
207
+ const ref = (ctx as unknown as { context?: unknown }).context;
208
+ expect(ref !== undefined).toBe(true);
209
+ });
210
+
211
+ // Our impl preserves the user-supplied options on the SecureContext;
212
+ // Node does not, so this is a GJS-specific guarantee we lean on for
213
+ // diagnostics + integration debugging.
214
+ await on('Gjs', async () => {
215
+ await it('preserves passed-through options (ciphers/minVersion/maxVersion)', async () => {
216
+ const ctx = createSecureContext({
217
+ ciphers: 'TLS_AES_128_GCM_SHA256',
218
+ minVersion: 'TLSv1.2',
219
+ maxVersion: 'TLSv1.3',
220
+ });
221
+ const o = (ctx as unknown as { options?: { ciphers?: string; minVersion?: string; maxVersion?: string } }).options;
222
+ expect(o).toBeDefined();
223
+ expect(o!.ciphers).toBe('TLS_AES_128_GCM_SHA256');
224
+ expect(o!.minVersion).toBe('TLSv1.2');
225
+ expect(o!.maxVersion).toBe('TLSv1.3');
226
+ });
227
+ });
228
+ });
229
+ });
230
+ };