postgres icon indicating copy to clipboard operation
postgres copied to clipboard

Support GSSAPI Authentication/encryption

Open Freddo3000 opened this issue 5 months ago • 4 comments

Postgres supports authentication and optionally encryption using GSSAPI, allowing authentication using Kerberos, SSPI and the like. I'm currently trying to create a PR using https://www.npmjs.com/package/kerberos which is used by mongodb-js, and poking around it seems like it should be possible to implement, though I've run into a bit of a roadblock when it comes to sending raw bytes as required by the GSSAPI authentication process.

WIP code diff
Index: src/connection.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/connection.js b/src/connection.js
--- a/src/connection.js	(revision 32feb259a3c9abffab761bd1758b3168d9e0cebc)
+++ b/src/connection.js	(date 1752946656209)
@@ -11,6 +11,8 @@
 import { Query, CLOSE } from './query.js'
 import b from './bytes.js'
 
+import {Kerberos} from "kerberos" // todo: make optional
+
 export default Connection
 
 let uid = 1
@@ -18,6 +20,7 @@
 const Sync = b().S().end()
     , Flush = b().H().end()
     , SSLRequest = b().i32(8).i32(80877103).end(8)
+    , GSSRequest = b().i32(8).i32(80877104).end(8)
     , ExecuteUnnamed = Buffer.concat([b().E().str(b.N).i32(0).end(), Sync])
     , DescribeUnnamed = b().D().str('S').str(b.N).end()
     , noop = () => { /* noop */ }
@@ -104,6 +107,8 @@
     , nonce = null
     , query = null
     , final = null
+    , kerberosClient = null
+    , kerberosEncClient = null
 
   const connection = {
     queue: queues.closed,
@@ -327,7 +332,6 @@
     terminated = false
     backendParameters = {}
     socket || (socket = await createSocket())
-
     if (!socket)
       return
 
@@ -353,7 +357,57 @@
     setTimeout(connect, closedDate ? closedDate + delay - performance.now() : 0)
   }
 
-  function connected() {
+  async function connectedGss() {
+    let gssToken
+    try {
+      kerberosEncClient = await Kerberos.initializeClient("postgres@" + host)
+      gssToken = await kerberosEncClient.step('')
+    } catch (e) {
+      return false
+    }
+
+    write(GSSRequest)
+    const canGSS = await new Promise(r => socket.once('data', x => r(x[0] === 71))) // G
+    if (!canGSS) {
+      return false
+    }
+    try {
+      while (!kerberosEncClient.contextComplete) {
+        let buffer = Buffer.from(gssToken, 'base64')
+        write(b().raw(buffer).end())
+        let ret = await new Promise(r => socket.once('data', x => r(x)))
+
+      }
+    } catch (e) {
+      console.log(e)
+      return false
+    }
+    const s = StartupMessage()
+    write(s)
+
+    statements = {}
+    needsTypes = options.fetch_types
+    statementId = Math.random().toString(36).slice(2)
+    statementCount = 1
+    lifeTimer.start()
+
+    socket.on('data', data)
+
+    keep_alive && socket.setKeepAlive && socket.setKeepAlive(true, 1000 * keep_alive)
+  }
+
+  async function connected() {
+    if (Kerberos !== null) {
+      try {
+        await connectedGss()
+      } catch (e) {
+        error(e)
+      }
+    }
     try {
       statements = {}
       needsTypes = options.fetch_types
@@ -654,6 +708,8 @@
     (
       type === 3 ? AuthenticationCleartextPassword :
       type === 5 ? AuthenticationMD5Password :
+      Kerberos !== null && type === 7 ? AuthenticationGss :
+      Kerberos !== null && type === 8 ? AuthenticationGssContinue :
       type === 10 ? SASL :
       type === 11 ? SASLContinue :
       type === 12 ? SASLFinal :
@@ -684,6 +740,22 @@
     )
   }
 
+  async function AuthenticationGss() {
+    console.assert(Kerberos !== null)
+
+    kerberosClient = await Kerberos.initializeClient("postgres/" + host) //todo
+    b().p().str('GSSAPI' + b.N)
+    const i = b.i
+    write(b.inc(1).str('p=' + kerberosClient.response).i32(b.i - i - 1, i).end())
+  }
+
+  async function AuthenticationGssContinue(x) {
+    console.assert(Kerberos !== null)
+    console.assert(kerberosClient !== null)
+
+
+  }
+
   async function SASL() {
     nonce = (await crypto.randomBytes(18)).toString('base64')
     b().p().str('SCRAM-SHA-256' + b.N)

The above code fails at write(b().raw(buffer).end()), and I'm not sure if returning data using ret = await new Promise(r => socket.once('data', x => r(x))) is correct.

ref:

  • https://www.postgresql.org/docs/current/gssapi-auth.html
  • https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-GSSAPI
  • https://www.postgresql.org/docs/17/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-GSSENCREQUEST

Freddo3000 avatar Jul 19 '25 17:07 Freddo3000

Just a heads up, while that package might be fine for a proof of concept I won't add dependencies, so it needs to be implemented without in as lean a way as possible.

Another route could be using the custom socket option and make a stand alone package for that which could then be plugged in.

porsager avatar Jul 19 '25 19:07 porsager

Fair enough. I was going for using package.json optionalDependencies to try and get around that limitation. How would you implement a custom socket and link that into the package? Just create a unix socket and connect to that or

Freddo3000 avatar Jul 19 '25 19:07 Freddo3000

yeah, the socket option supports an async function that should resolve with the custom socket, so you should be able to do anything - eg usage could look like this:

import postgres from 'postgres'
import kerberos from 'postgres-kerberos'

const sql = postgres({
  socket: kerberos({
    kerberosOptions
  })
})

porsager avatar Jul 19 '25 19:07 porsager

That'd probably work for establishing encryption at least, though I'd still need to perform the GSSAPI Authentication step as well once the socket is opened.

Freddo3000 avatar Jul 19 '25 20:07 Freddo3000