-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add support for SCRAM-SHA-256-PLUS i.e. channel binding #3356
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
Changes from 4 commits
65e1c72
a338521
8ab2b17
37f9285
ae6ab3f
152396f
f899f8f
52e656c
1003bff
50ee305
b3a8757
67a6e9c
b604068
88cbe49
05690bb
d9fdccf
9a91cb7
8fc5f5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,41 @@ | ||
| 'use strict' | ||
| const crypto = require('./utils') | ||
| const tls = require('tls') | ||
| const x509 = require('@peculiar/x509') | ||
|
|
||
| function startSession(mechanisms) { | ||
| if (mechanisms.indexOf('SCRAM-SHA-256') === -1) { | ||
| throw new Error('SASL: Only mechanism SCRAM-SHA-256 is currently supported') | ||
| function startSession(mechanisms, stream) { | ||
| const candidates = ['SCRAM-SHA-256'] | ||
| if (stream) candidates.unshift('SCRAM-SHA-256-PLUS') // higher-priority, so placed first | ||
|
|
||
| let mechanism | ||
| for (const candidate of candidates) { | ||
| if (mechanisms.indexOf(candidate) !== -1) { | ||
| mechanism = candidate | ||
| break | ||
| } | ||
| } | ||
jawj marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (!mechanism) { | ||
| throw new Error('SASL: Only mechanisms ' + candidates.join(' and ') + ' are supported') | ||
| } | ||
|
|
||
| if (mechanism === 'SCRAM-SHA-256-PLUS' && !(stream instanceof tls.TLSSocket)) { | ||
jawj marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // this should never happen if we are really talking to a Postgres server | ||
| throw new Error('SASL: Mechanism SCRAM-SHA-256-PLUS requires a secure connection') | ||
| } | ||
|
|
||
| const clientNonce = crypto.randomBytes(18).toString('base64') | ||
| const gs2Header = mechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : stream ? 'y' : 'n' | ||
|
|
||
| return { | ||
| mechanism: 'SCRAM-SHA-256', | ||
| mechanism, | ||
| clientNonce, | ||
| response: 'n,,n=*,r=' + clientNonce, | ||
| response: gs2Header + ',,n=*,r=' + clientNonce, | ||
| message: 'SASLInitialResponse', | ||
| } | ||
| } | ||
|
|
||
| async function continueSession(session, password, serverData) { | ||
| async function continueSession(session, password, serverData, stream) { | ||
| if (session.message !== 'SASLInitialResponse') { | ||
| throw new Error('SASL: Last message was not SASLInitialResponse') | ||
| } | ||
|
|
@@ -40,7 +59,33 @@ async function continueSession(session, password, serverData) { | |
|
|
||
| var clientFirstMessageBare = 'n=*,r=' + session.clientNonce | ||
| var serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration | ||
| var clientFinalMessageWithoutProof = 'c=biws,r=' + sv.nonce | ||
|
|
||
| // without channel binding: | ||
| let channelBinding = stream ? 'eSws' : 'biws' // 'y,,' or 'n,,', base64-encoded | ||
|
|
||
| // 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) | ||
|
||
| const sigAlgo = parsedCert.signatureAlgorithm | ||
| if (!sigAlgo) { | ||
| throw new Error('Could not extract signature algorithm from certificate') | ||
| } | ||
| const hash = sigAlgo.hash | ||
| if (!hash) { | ||
| throw new Error('Could not extract hash from certificate signature algorithm') | ||
| } | ||
| let hashName = hash.name | ||
| if (!hashName) { | ||
| throw new Error('Could not extract name from certificate signature algorithm hash') | ||
| } | ||
| if (/^(md5)|(sha-?1)$/i.test(hashName)) hashName = 'SHA-256' // for MD5 and SHA-1, we substitute SHA-256 | ||
jawj marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const certHash = await crypto.hashByName(hashName, peerCert) | ||
| const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)]) | ||
| channelBinding = bindingData.toString('base64') | ||
| } | ||
|
|
||
| var clientFinalMessageWithoutProof = 'c=' + channelBinding + ',r=' + sv.nonce | ||
| var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof | ||
|
|
||
| var saltBytes = Buffer.from(sv.salt, 'base64') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 () => { | ||
|
||
| const client = new pg.Client(config) | ||
| let usingSasl = false | ||
| client.connection.once('authenticationSASL', () => { | ||
| usingSasl = true | ||
| }) | ||
| client.enableChannelBinding = true | ||
| await client.connect() | ||
| assert.ok(usingSasl, 'Should be using SASL for authentication') | ||
| await client.end() | ||
| }) | ||
|
|
||
| suite.testAsync('can connect using sasl/scram (channel binding disabled)', async () => { | ||
| const client = new pg.Client(config) | ||
| let usingSasl = false | ||
| client.connection.once('authenticationSASL', () => { | ||
| usingSasl = true | ||
| }) | ||
| client.enableChannelBinding = false // default | ||
| await client.connect() | ||
| assert.ok(usingSasl, 'Should be using SASL for authentication') | ||
| await client.end() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.