pscl.js and the ProScript API

Using pscl.js

We do not use pscl.js in dirsp-exchange! The ProScript Cryptography Library (PSCL) is used as an API during ProVerif proof testing, and we use it as an API (see the OCaml library dirsp-proscript). There is a dirsp-proscript-mirage that uses the Mirage crypto libraries, which themselves use a correct-by-construction implementation from fiat-crypto for its elliptic curve functions.

We decided to abandon pscl.js after trying to compare its output with ours. Some cautionary notes:

  1. Comparing dirsp-exchange’s output to sp.js + pscl.js is a bit of a fool’s errand. After burning two days trying to match results, I realized that pscl.js’ DH25519 was non-standard. In fact, pscl.js is not used for the security proofs (the ProVerif translation will replace references to PSCL functions like DH25519). See comment about DH25519 in https://github.com/Inria-Prosecco/proscript-messaging/issues/1 ; I had likewise ran test vectors through pscl.js and its DH25519 failed.

  2. It was difficult to understand which parameters to pscl.js need to be hex-encoded and which ones don’t. And it really matters in JavaScript. In node.js, the new Buffer(some_variable, 'hex') construction used frequently in pscl.js will silently return an empty buffer if the input is not hex. For example, supplying values without hex encoding to ProScript.crypto.ED25519.signature would result in a call to ProScript.crypto.ED25519Hash(sk), which in turn would return a SHA512 hash of an empty value because of the Buffer-based hex decoding. Yet the ProVerif code does not hex encode that sk parameter when calling the ProScript.crypto.ED25519.signature API.

  3. Completely as a reaction to hex-encoding ambiguities in pscl.js, nothing is expected to be hex encoded in dirsp-proscript. The dirsp-proscript API is just raw bytes. However, the hex encoders toBitstring and fromBitstring used in the higher-level KBB2017 algorithm (ps/sp.js) are still present.

So there is no need for pscl.js, and you won’t get matching results because the kinda critical Diffie-Hellman implementation in pscl.js is non-standard and perhaps broken.

Sidebar: None of the above caution affects the KBB2017 proof

Now that the cautionary preamble is out of the way … here is how you would run sp.js + pscl.js.

You’ll need node.js v10+ installed.

Then:

npm install --save-dev window hexy
npm install --save-dev @peculiar/webcrypto # only needed if node.js v14 or earlier
node --experimental-repl-await # or 'nvm exec 16.0 node --experimental-repl-await' depending on your installation

Within node:

// EITHER: this section is for node.js v10-v14
const getRandomValues = await require('get-random-values')

// OR: this section is for node.js v15+ which has built-in (more vetted) webcrypto support
const { getRandomValues } = await require('crypto').webcrypto

// THEN continue with the following ...

// Make an approximation of the browser
const Window = await require('window')
const { createHash, createHmac, createCipheriv, createDecipheriv } = await require('crypto');
const window = new Window()
window.crypto = { getRandomValues }
const NodeCrypto = { createHash, createHmac, createCipheriv, createDecipheriv }

// Load the Javascript files
const fs = await require("fs")
const pscl_js = fs.readFileSync('./src-proscript/proscript-messaging/pscl/pscl.js', 'utf8')
eval(pscl_js)
const sp_js = fs.readFileSync('./src-proscript/proscript-messaging//ps/sp.js', 'utf8')
eval(sp_js.replaceAll(/\bconst /g,"var "))

// Now you have access to the ProScript Cryptographic Library (pscl.js)
// and the 'Signal Protocol'/KBB2017 (sp.js)
ProScript.crypto.random16Bytes()
Type_key.construct()
const { hexy } = await require('hexy')
hexy(Type_key.construct())

// You can also override the random functions so you can do a
// repeatable comparison between JavaScript and OCaml
const firstByteMd5 = function(s) { return NodeCrypto.createHash('md5').update(s).digest()[0] }
ProScript.crypto.random12Bytes = function(id) { const fb = firstByteMd5(id); return Array.from({length: 12}, () => fb) }
ProScript.crypto.random16Bytes = function(id) { const fb = firstByteMd5(id); return Array.from({length: 16}, () => fb) }
ProScript.crypto.random32Bytes = function(id) { const fb = firstByteMd5(id); return Array.from({length: 32}, () => fb) }

You can simulate a conversation between Alice and Bob:

const P   = ProScript
const C   = P.crypto
const E   = P.crypto.ED25519
const U   = UTIL
const T   = TOPLEVEL
const KEY = Type_key
const MSG = Type_msg

let aliceIdentityKey  = U.newIdentityKey ("alice-identity")
let aliceSignedPreKey = U.newKeyPair     ("alice-signed-prekey")
let bobIdentityKey    = U.newIdentityKey ("bob-identity")
let bobSignedPreKey   = U.newKeyPair     ("bob-signed-prekey")

let aliceIdentityKeyPub        = aliceIdentityKey.pub
let aliceIdentityDHKeyPub      = U.getDHPublicKey (aliceIdentityKey.priv)
let aliceSignedPreKeyPub       = aliceSignedPreKey.pub
let aliceSignedPreKeySignature = E.signature (KEY.toBitstring(aliceSignedPreKeyPub), aliceIdentityKey.priv, aliceIdentityKeyPub)
let bobIdentityKeyPub          = bobIdentityKey.pub
let bobIdentityDHKeyPub        = U.getDHPublicKey(bobIdentityKey.priv)
let bobSignedPreKeyPub         = bobSignedPreKey.pub
let bobSignedPreKeySignature   = E.signature (KEY.toBitstring(bobSignedPreKeyPub  ), bobIdentityKey.priv,   bobIdentityKeyPub)

let alicePreKey   = U.newKeyPair     ("alice-prekey")
let bobPreKey     = U.newKeyPair     ("bob-prekey")

let alicePreKeyPub = KEY.toBitstring (alicePreKey.pub)
let alicePreKeyId  = 1 /* the Id of alicePreKey */
let bobPreKeyPub   = KEY.toBitstring (bobPreKey.pub)
let bobPreKeyId    = 1 /* the Id of bobPreKey   */

let aliceSessionWithBob = T.newSession(
    aliceSignedPreKey,
    alicePreKey,
    KEY.toBitstring (bobIdentityKeyPub),
    KEY.toBitstring (bobIdentityDHKeyPub),
    KEY.toBitstring (bobSignedPreKeyPub),
    bobSignedPreKeySignature,
    KEY.toBitstring (bobPreKeyPub),
    bobPreKeyId
    )
let bobSessionWithAlice = T.newSession(
    bobSignedPreKey,
    bobPreKey,
    KEY.toBitstring (aliceIdentityKeyPub),
    KEY.toBitstring (aliceIdentityDHKeyPub),
    KEY.toBitstring (aliceSignedPreKeyPub),
    aliceSignedPreKeySignature,
    KEY.toBitstring (alicePreKeyPub),
    alicePreKeyId
    )

let aliceToBobMsg1        = "Hi Bob!"
let aliceToBobSendOutput1 = T.send(
    aliceIdentityKey,
    aliceSessionWithBob,
    aliceToBobMsg1
)
console.log(util.format("Did Alice send successfully? %s\n", aliceToBobSendOutput1.output.valid))

let bobFromAliceMsg2           = aliceToBobSendOutput1.output
let bobFromAliceReceiveOutput2 = T.recv(
  bobIdentityKey,
  bobSignedPreKey,
  bobSessionWithAlice,
  bobFromAliceMsg2,
)
console.log(util.format("Did Bob receive successfully? %s\n", bobFromAliceReceiveOutput2.output.valid))
/* this should work, but doesn't */

Suggested Improvements

We don’t control the development of ProScript. So here are the feature requests we would send to the ProScript team:

  1. If pscl.js is going to be used by anyone, it needs to do a length check on the result of any Buffer conversion. In fact, node.js complains that “Buffer() is deprecated due to security and usability issues”, so removing the code would be better.

  2. Let’s statically type or annotate which parameters are hex-encoded. It is _way_ too easy to make a mistake where you pass in a hex encoded input to a parameter that doesn’t need hex encoding, or you hex encode something twice.