Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SCRAM-SHA-256-PLUS i.e. channel binding #3356

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

jawj
Copy link

@jawj jawj commented Jan 10, 2025

Hi all. I hope you'll consider this patch, which adds support for SCRAM-SHA-256-PLUS.

SCRAM-SHA-256-PLUS on Postgres enables tls-server-end-point channel binding, where the client sends the server a hash of the certificate it received as part of the TLS handshake. This prevents some kinds of MITM attacks where the attacker obtains a certificate that appears valid for the server, but is not actually the server's.

So far I've tested it working against Neon (who support SCRAM-SHA-256-PLUS) and Supabase (who don't), and with the new client.enableChannelBinding flag both true and false.

Feel free to make any changes you think appropriate.

@jawj jawj changed the title Added support for SCRAM-SHA-256-PLUS i.e. channel binding Add support for SCRAM-SHA-256-PLUS i.e. channel binding Jan 10, 2025
@@ -20,6 +20,7 @@
"author": "Brian Carlson <[email protected]>",
"main": "./lib",
"dependencies": {
"@peculiar/x509": "^1.12.3",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should absolutely pin this dependency - i only allow packages within this repo to float patch versions w/ semver. Anything external needs to be vetted & checked on exact versions, and using exact versions in package.json please!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also comments below.

Copy link
Owner

@brianc brianc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the PR! Put a few comments on here. My main concern is changing the default behavior - as much as its a pain on users to enable the feature, I'd rather it be an opt-in thing vs a default...otherwise it's a slight breaking change, which requires a new semver major. Would be nice to add a callout in the docs about this as well...though I can handle that if you don't want to

@jawj
Copy link
Author

jawj commented Jan 14, 2025

Thanks @brianc. I've made those changes and added a short section to the docs.

However, while writing the docs it struck me that I have only provided the opt-in option directly on Client. How should this feature be exposed for those using pool.query?

packages/pg/lib/crypto/sasl.js Outdated Show resolved Hide resolved
packages/pg/lib/crypto/sasl.js Outdated Show resolved Hide resolved
throw new Error('SASL: Only mechanisms ' + candidates.join(' and ') + ' are supported')
}

if (mechanism === 'SCRAM-SHA-256-PLUS' && !(stream instanceof tls.TLSSocket)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this instanceof check important for correctness? (It doesn’t look like it is, but I’m not overly familiar with the flow.) It’s generally nice for users to be able to provide stream objects that satisfy the API pg uses without strictly needing them to inherit from a particular type (or pretend to).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, so let's change to this, which only checks that the stream object has the method we need:

  if (mechanism === 'SCRAM-SHA-256-PLUS' && typeof stream.getPeerCertificate !== 'function') {
    // this should never happen if we are really talking to a Postgres server
    throw new Error('SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate')
  }

// override if channel binding is in use:
if (session.mechanism === 'SCRAM-SHA-256-PLUS') {
const peerCert = stream.getPeerCertificate().raw
const parsedCert = new x509.X509Certificate(peerCert)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a separate package, could stream.getPeerCertificate().fingerprint256 be used with a fixed selection of SHA-256? Or is that not the same hash?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind – that wouldn’t be spec-compliant and I missed that the hash wasn’t being used for anything more than its name anyway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do appreciate that bringing in a whole X509 parsing library (and then parsing the whole certificate, when all we actually need is the signature algorithm) feels like overkill.

I did actually have a go at doing the minimum necessary parsing manually: see https://gist.github.com/jawj/04a90e51196ac054d6741c8d079d9cff

The reason I didn't go with that in the PR is that I haven't been able to find a list of either (a) what signature algorithms could theoretically be used or even (b) what signature algorithms would cover 99% of cases.

But I strongly suspect that the cases covered by this code would be almost all of them, and any missing ones might be plugged if people filed issues about them. So this could be another option?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I strongly suspect that the cases covered by this code would be almost all of them, and any missing ones might be plugged if people filed issues about them. So this could be another option?

I am absolutely totally down w/ the "make it mostly work and patch if different algorithms show up later" mode if it removes the requirement to pull in an entire dependency! I wouldn't say it's a mandatory change but certainly would be welcome. 😄 I have tried very hard over the years to keep as many 3rd party dependencies out of the code as possible just because....well...left-pad and all that stuff, ya know?

packages/pg/lib/client.js Outdated Show resolved Hide resolved
@@ -45,12 +45,25 @@ if (!config.user || !config.password) {
return
}

suite.testAsync('can connect using sasl/scram', async () => {
suite.testAsync('can connect using sasl/scram (channel binding enabled)', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests would work just as well if there were no implementation of channel binding at all, so this PR probably needs a targeted test from someone.

@brianc
Copy link
Owner

brianc commented Jan 17, 2025

How should this feature be exposed for those using pool.query?

hmm good question - would need to make it something that's thread through the pool to its create method as well. The pool passes its own constructor options to the client which probably if i were designing the API today from scratch I wouldn't do but..that ship sailed like 10 years ago 🙃 - so it should just be able to be set on the pool as well?

@brianc
Copy link
Owner

brianc commented Jan 17, 2025

@jawj ruh roh! looks like a test is failing (should be an easy fix). There are a lot of tests - some are duplicates, etc. If you just wanna run a subset locally for faster turn around you can run make test-unit and/or make test-integration from the ./packages/pg subfolder. In this case it was a unit test that broke (meaning you don't need an instance of postgres running locally)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants