-
Notifications
You must be signed in to change notification settings - Fork 429
Description
I have been trying to connect from a cloudflare worker to postgres, using SCRAM (which is the new default since PG14).
For that, a modified version of https://github.com/cloudflare/worker-template-postgres was used, which showed that PG with SCRAM auth method is never able to get connected to from the worker, while MD5 and passwordless methods worked just fine.
When similar example was run from Deno, it worked for all auth methods, including SCRAM with the same postgres.
I've narrowed down the case to the following snippet:
const text_encoder = new TextEncoder();
// interface KeySignatures {
// client: Uint8Array;
// server: Uint8Array;
// stored: Uint8Array;
// }
// See https://github.com/denodrivers/postgres/blob/8a07131efa17f4a6bcab86fd81407f149de93449/connection/scram.ts#L93
// async function deriveKeySignatures(
// password: string,
// salt: Uint8Array,
// iterations: number,
// ): Promise<KeySignatures> {
async function deriveKeySignatures(
password,
salt,
iterations,
) {
console.log(`Input: \n${JSON.stringify({
password,
salt: arrayToPrettyString(salt),
iterations
}, false, 2)}\n================\n`);
const pbkdf2_password = await crypto.subtle.importKey(
"raw",
text_encoder.encode(password),
"PBKDF2",
false,
["deriveBits", "deriveKey"],
);
const key = await crypto.subtle.deriveKey(
{
hash: "SHA-256",
iterations,
name: "PBKDF2",
salt,
},
pbkdf2_password,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const client = new Uint8Array(
await crypto.subtle.sign("HMAC", key, text_encoder.encode("Client Key")),
);
const server = new Uint8Array(
await crypto.subtle.sign("HMAC", key, text_encoder.encode("Server Key")),
);
const stored = new Uint8Array(await crypto.subtle.digest("SHA-256", client));
return { client, server, stored };
}
////////////////////////
// See https://www.rfc-editor.org/rfc/rfc7677
// See https://github.com/denodrivers/postgres/blob/8a07131efa17f4a6bcab86fd81407f149de93449/tests/auth_test.ts#L6
function decode(b64) {
const binString = atob(b64);
const size = binString.length;
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) {
bytes[i] = binString.charCodeAt(i);
}
return bytes;
}
const password = 'pencil'
const salt = decode('W22ZaJ0SNY7soEsUEjb6gQ==');
const iterations = 4096
function signaturesToString(signatures) {
let { client, server, stored } = signatures;
return JSON.stringify({
client: arrayToPrettyString(client),
server: arrayToPrettyString(server),
stored: arrayToPrettyString(stored),
}, false, 2);
}
function arrayToPrettyString(array) {
return `[${Array.apply([], array).join(",")}]`
}
//////////////////////// DENO
(async () => {
const signatures = await deriveKeySignatures(password, salt, iterations);
console.log(`Output: \n${signaturesToString(signatures)}`);
})();
//////////////////////// workerd
addEventListener("fetch", async event => {
const signatures = await deriveKeySignatures(password, salt, iterations);
const signaturesString = signaturesToString(signatures);
console.log(`Output: \n${signaturesString}`);
event.respondWith(new Response(signaturesString));
});
The snippet reuses slightly modified code from postgres deno driver:
https://github.com/denodrivers/postgres/blob/8a07131efa17f4a6bcab86fd81407f149de93449/connection/scram.ts#L93
and a test for that: https://github.com/denodrivers/postgres/blob/8a07131efa17f4a6bcab86fd81407f149de93449/tests/auth_test.ts#L6
based on https://www.rfc-editor.org/rfc/rfc7677
When launched from deno
, it outputs the following:
~/work/workerd_configs master* ❯ deno run ./crypto/crypto.ts
Input:
{
"password": "pencil",
"salt": "[91,109,153,104,157,18,53,142,236,160,75,20,18,54,250,129]",
"iterations": 4096
}
================
Output:
{
"client": "[166,15,201,35,214,126,134,68,169,45,22,185,110,218,94,244,101,107,12,114,92,72,67,116,190,37,83,85,118,153,110,139]",
"server": "[193,243,203,193,193,58,157,53,161,76,9,144,238,217,118,41,234,34,88,99,229,102,164,49,74,185,159,63,0,229,217,213]",
"stored": "[88,110,93,242,131,230,220,235,92,62,121,29,139,133,40,236,25,30,102,64,69,206,151,23,146,226,230,181,187,19,226,166]"
}
When deployed to the cloudflare worker, it outputs the following (sometimes, twice):
Input:
{
"password": "pencil",
"salt": "[91,109,153,104,157,18,53,142,236,160,75,20,18,54,250,129]",
"iterations": 4096
}
================
worker.js:96 Output:
{
"client": "[39,109,21,199,34,123,195,118,110,149,158,193,245,83,213,158,108,10,144,29,179,78,203,90,42,67,86,166,43,73,194,174]",
"server": "[191,207,32,144,249,160,146,168,90,192,19,74,137,113,122,251,66,75,42,5,24,220,131,66,45,238,213,39,31,56,245,224]",
"stored": "[177,174,171,65,56,228,4,105,247,107,133,55,230,13,215,114,1,133,143,68,20,119,43,239,251,225,142,245,250,194,40,71]"
}
When run with workerd (built from f7bd5f4) locally, it outputs the following:
work/workerd_configs/crypto master* ❯ cat crypto.capnp
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "main", worker = .mainWorker),
],
sockets = [
# Serve HTTP on port 8080.
( name = "http",
address = "*:8080",
http = (),
service = "main"
),
]
);
const mainWorker :Workerd.Worker = (
serviceWorkerScript = embed "crypto.ts",
compatibilityDate = "2022-09-16",
);
work/workerd_configs/crypto master* ❯ ../../workerd/bazel-bin/src/workerd/server/workerd serve "crypto.capnp"
workerd/io/worker-entrypoint.c++:209: error: e = kj/async-io-unix.c++:1652: failed: connect() blocked by restrictPeers()
stack: 104c30cf4 104c32160 104c1d5b8 104c1da5c 1049fbb6c 104a0c764 104a2f6c0 104a37308 104a540ac 104c0f8cc 104a540ac 102b59fa0; sentryErrorContext = workerEntrypoint
workerd/server/server.c++:1834: error: Uncaught exception: kj/async-io-unix.c++:1652: failed: worker_do_not_log; Request failed due to internal error
stack: 104c30cf4 104c32160 104c1d5b8 104c1da5c 1049fbb6c 104a0c764 104a2f6c0 104a37308 104a540ac 104c0f8cc 104a540ac 102b59fa0 104a066cc 104a097f0
workerd/io/worker-entrypoint.c++:209: error: e = kj/async-io-unix.c++:1652: failed: connect() blocked by restrictPeers()
stack: 104c30cf4 104c32160 104c1d5b8 104c1da5c 1049fbb6c 104a0c764 104a2f6c0 104a37308 104a540ac 104c0f8cc 104a540ac 102b59fa0; sentryErrorContext = workerEntrypoint
workerd/server/server.c++:1834: error: Uncaught exception: kj/async-io-unix.c++:1652: failed: worker_do_not_log; Request failed due to internal error
stack: 104c30cf4 104c32160 104c1d5b8 104c1da5c 1049fbb6c 104a0c764 104a2f6c0 104a37308 104a540ac 104c0f8cc 104a540ac 102b59fa0 104a066cc 104a097f0
and returns internal server error.
This is a bug, according to https://github.com/cloudflare/workerd/blame/f7bd5f40adb3909cefcc63b7b06a2d6c1baafbd5/README.md#L40
"internal errors" (bugs in the implementation which the Workers team should address)
I would expect both locally built workerd and the one from cloudflare to compute the signatures with their values identical to what Deno script produces for the same input.