Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"plugin:prettier/recommended",
"prettier"
],
"ignorePatterns": [
"**/*.d.ts",
"**/*.js",
"coverage/*",
"lib/*"
],
"rules": {
"@typescript-eslint/member-delimiter-style": 0,
"@typescript-eslint/no-namespace": [
Expand Down
183 changes: 93 additions & 90 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build": "webpack",
"pretest": "npm run sjcl",
"test": "tsc -b && node --experimental-vm-modules node_modules/jest/bin/jest.js --ci",
"lint": "eslint ./src/**/*.{ts,tsx}",
"lint": "eslint .",
"clean": "rimraf dist"
},
"dependencies": {
Expand Down
132 changes: 132 additions & 0 deletions src/blindrsa/blindrsa.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import blindRSA from './index';
import { jest } from '@jest/globals';
import sjcl from './sjcl';
// Test vector
// https://www.ietf.org/archive/id/draft-irtf-cfrg-rsa-blind-signatures-03.html#appendix-A
import vectors from './testdata/rsablind_vectors.json';

function hexToB64URL(x: string): string {
return Buffer.from(x, 'hex').toString('base64url');
}

function hexToUint8(x: string): Uint8Array {
return new Uint8Array(Buffer.from(x, 'hex'));
}

function paramsFromVector(v: typeof vectors[number]): {
n: string;
e: string;
d: string;
p: string;
q: string;
dp: string;
dq: string;
qi: string;
} {
const n = hexToB64URL(v.n);
const e = hexToB64URL(v.e);
const d = hexToB64URL(v.d);
const p = hexToB64URL(v.p);
const q = hexToB64URL(v.q);

// Calculate CRT values
const bnD = new sjcl.bn(v.d);
const bnP = new sjcl.bn(v.p);
const bnQ = new sjcl.bn(v.q);
const one = new sjcl.bn(1);
const dp = hexToB64URL(bnD.mod(bnP.sub(one)).toString());
const dq = hexToB64URL(bnD.mod(bnQ.sub(one)).toString());
const qi = hexToB64URL(bnQ.inverseMod(bnP).toString());
return { n, e, d, p, q, dp, dq, qi };
}

async function keysFromVector(
v: typeof vectors[number],
extractable: boolean,
): Promise<CryptoKeyPair> {
const params = paramsFromVector(v);
const { n, e } = params;
const publicKey = await crypto.subtle.importKey(
'jwk',
{ kty: 'RSA', ext: true, n, e },
{ name: 'RSA-PSS', hash: 'SHA-384' },
extractable,
['verify'],
);

const privateKey = await crypto.subtle.importKey(
'jwk',
{ kty: 'RSA', ext: true, ...params },
{ name: 'RSA-PSS', hash: 'SHA-384' },
extractable,
['sign'],
);
return { privateKey, publicKey };
}

describe.each(vectors)('BlindRSA-vec$#', (v: typeof vectors[number]) => {
test('test-vector', async () => {
const r_inv = new sjcl.bn(v.inv);
const r = r_inv.inverseMod(new sjcl.bn(v.n));
const r_bytes = hexToUint8(r.toString().slice(2));

const { privateKey, publicKey } = await keysFromVector(v, true);
const msg = hexToUint8(v.msg);
const saltLength = v.salt.length / 2;

// Mock for randomized blind operation.
jest.spyOn(crypto, 'getRandomValues')
.mockReturnValueOnce(hexToUint8(v.salt)) // mock for random salt
.mockReturnValueOnce(r_bytes); // mock for random blind

const { blindedMsg, blindInv } = await blindRSA.blind(publicKey, msg, saltLength);
expect(blindedMsg).toStrictEqual(hexToUint8(v.blinded_msg));
expect(blindInv).toStrictEqual(hexToUint8(v.inv));

const blindedSig = await blindRSA.blindSign(privateKey, blindedMsg);
expect(blindedSig).toStrictEqual(hexToUint8(v.blind_sig));

const signature = await blindRSA.finalize(publicKey, msg, blindInv, blindedSig, saltLength);
expect(signature).toStrictEqual(hexToUint8(v.sig));
});

test('non-extractable-keys', async () => {
const { privateKey, publicKey } = await keysFromVector(v, false);
const msg = crypto.getRandomValues(new Uint8Array(10));
const blindedMsg = crypto.getRandomValues(new Uint8Array(32));
const blindInv = crypto.getRandomValues(new Uint8Array(32));
const blindedSig = crypto.getRandomValues(new Uint8Array(32));
const errorMsg = 'key is not extractable';

await expect(blindRSA.blind(publicKey, msg, 32)).rejects.toThrow(errorMsg);
await expect(blindRSA.blindSign(privateKey, blindedMsg)).rejects.toThrow(errorMsg);
await expect(blindRSA.finalize(publicKey, msg, blindInv, blindedSig, 32)).rejects.toThrow(
errorMsg,
);
});

test('wrong-key-type', async () => {
const { privateKey, publicKey } = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5', // not RSA-PSS.
modulusLength: 2048,
publicExponent: Uint8Array.from([0x01, 0x00, 0x01]),
hash: 'SHA-256',
},
true,
['sign', 'verify'],
);

const msg = crypto.getRandomValues(new Uint8Array(10));
const blindedMsg = crypto.getRandomValues(new Uint8Array(32));
const blindInv = crypto.getRandomValues(new Uint8Array(32));
const blindedSig = crypto.getRandomValues(new Uint8Array(32));
const errorMsg = 'key is not RSA-PSS';

await expect(blindRSA.blind(publicKey, msg, 32)).rejects.toThrow(errorMsg);
await expect(blindRSA.blindSign(privateKey, blindedMsg)).rejects.toThrow(errorMsg);
await expect(blindRSA.finalize(publicKey, msg, blindInv, blindedSig, 32)).rejects.toThrow(
errorMsg,
);
});
});
156 changes: 156 additions & 0 deletions src/blindrsa/blindrsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { emsa_pss_encode, i2osp, os2ip, rsasp1, rsavp1 } from './util';

import sjcl from './sjcl';

export async function blind(
publicKey: CryptoKey,
msg: Uint8Array,
saltLength = 0,
): Promise<{
blindedMsg: Uint8Array;
blindInv: Uint8Array;
}> {
if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'RSA-PSS') {
throw new Error('key is not RSA-PSS');
}
if (!publicKey.extractable) {
throw new Error('key is not extractable');
}

const { modulusLength, hash: hashFn } = publicKey.algorithm as RsaHashedKeyGenParams;
const kBits = modulusLength;
const kLen = Math.ceil(kBits / 8);
const hash = (hashFn as Algorithm).name;

// 1. encoded_msg = EMSA-PSS-ENCODE(msg, kBits - 1)
// with MGF and HF as defined in the parameters
// 2. If EMSA-PSS-ENCODE raises an error, raise the error and stop
const encoded_msg = await emsa_pss_encode(msg, kBits - 1, { sLen: saltLength, hash });

// 3. m = bytes_to_int(encoded_msg)
const m = os2ip(encoded_msg);
const jwkKey = await crypto.subtle.exportKey('jwk', publicKey);
if (!jwkKey.n || !jwkKey.e) {
throw new Error('key has invalid parameters');
}
const n = new sjcl.bn(Buffer.from(jwkKey.n, 'base64url').toString('hex'));
const e = new sjcl.bn(Buffer.from(jwkKey.e, 'base64url').toString('hex'));

// 4. r = random_integer_uniform(1, n)
let r: sjcl.bn;
do {
r = os2ip(crypto.getRandomValues(new Uint8Array(kLen)));
} while (r.greaterEquals(n));

// 5. r_inv = inverse_mod(r, n)
// 6. If inverse_mod fails, raise an "invalid blind" error
// and stop
let r_inv: sjcl.bn;
try {
r_inv = r.inverseMod(new sjcl.bn(n));
} catch (e) {
throw new Error('invalid blind');
}
// 7. x = RSAVP1(pkS, r)
const x = rsavp1({ n, e }, r);

// 8. z = m * x mod n
const z = m.mulmod(x, n);

// 9. blinded_msg = int_to_bytes(z, kLen)
const blindedMsg = i2osp(z, kLen);

// 10. inv = int_to_bytes(r_inv, kLen)
const blindInv = i2osp(r_inv, kLen);

// 11. output blinded_msg, inv
return { blindedMsg, blindInv };
}

export async function finalize(
publicKey: CryptoKey,
msg: Uint8Array,
blindInv: Uint8Array,
blindSig: Uint8Array,
saltLength = 0,
): Promise<Uint8Array> {
if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'RSA-PSS') {
throw new Error('key is not RSA-PSS');
}
if (!publicKey.extractable) {
throw new Error('key is not extractable');
}
const { modulusLength } = publicKey.algorithm as RsaHashedKeyGenParams;
const kLen = Math.ceil(modulusLength / 8);

// 1. If len(blind_sig) != kLen, raise "unexpected input size" and stop
// 2. If len(inv) != kLen, raise "unexpected input size" and stop
if (blindSig.length != kLen || blindInv.length != kLen) {
throw new Error('unexpected input size');
}

// 3. z = bytes_to_int(blind_sig)
const z = os2ip(blindSig);

// 4. r_inv = bytes_to_int(inv)
const r_inv = os2ip(blindInv);

// 5. s = z * r_inv mod n
const jwkKey = await crypto.subtle.exportKey('jwk', publicKey);
if (!jwkKey.n) {
throw new Error('key has invalid parameters');
}
const n = new sjcl.bn(Buffer.from(jwkKey.n, 'base64url').toString('hex'));
const s = z.mulmod(r_inv, n);

// 6. sig = int_to_bytes(s, kLen)
const sig = i2osp(s, kLen);

// 7. result = RSASSA-PSS-VERIFY(pkS, msg, sig)
// 8. If result = "valid signature", output sig, else
// raise "invalid signature" and stop
const algorithm = { name: 'RSA-PSS', saltLength };
if (!(await crypto.subtle.verify(algorithm, publicKey, sig, msg))) {
throw new Error('invalid signature');
}

return sig;
}

export async function blindSign(privateKey: CryptoKey, blindMsg: Uint8Array): Promise<Uint8Array> {
if (privateKey.type !== 'private' || privateKey.algorithm.name !== 'RSA-PSS') {
throw new Error('key is not RSA-PSS');
}
if (!privateKey.extractable) {
throw new Error('key is not extractable');
}
const { modulusLength } = privateKey.algorithm as RsaHashedKeyGenParams;
const kLen = Math.ceil(modulusLength / 8);

// 1. If len(blinded_msg) != kLen, raise "unexpected input size"
// and stop
if (blindMsg.length != kLen) {
throw new Error('unexpected input size');
}

// 2. m = bytes_to_int(blinded_msg)
const m = os2ip(blindMsg);

// 3. If m >= n, raise "invalid message length" and stop
const jwkKey = await crypto.subtle.exportKey('jwk', privateKey);
if (!jwkKey.n || !jwkKey.d) {
throw new Error('key is not a private key');
}
const n = new sjcl.bn(Buffer.from(jwkKey.n, 'base64url').toString('hex'));
const d = new sjcl.bn(Buffer.from(jwkKey.d, 'base64url').toString('hex'));
if (m.greaterEquals(n)) {
throw new Error('invalid message length');
}

// 4. s = RSASP1(skS, m)
const s = rsasp1({ n, d }, m);

// 5. blind_sig = int_to_bytes(s, kLen)
// 6. output blind_sig
return i2osp(s, kLen);
}
3 changes: 3 additions & 0 deletions src/blindrsa/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { blind, blindSign, finalize } from './blindrsa';
export default { blindSign, finalize, blind };
export { blind, blindSign, finalize } from './blindrsa';
16 changes: 16 additions & 0 deletions src/blindrsa/sjcl.Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
SJCL_PATH=node_modules/sjcl

all:
cd ${SJCL_PATH} && \
./configure --without-all --with-ecc --with-convenience --compress=none \
--with-codecBytes --with-codecHex --with-codecArrayBuffer && \
make
npm i -D dts-gen
npx dts-gen -m sjcl -o -f ./src/sjcl/index
npm un -D dts-gen
echo "export default sjcl;" >> ${SJCL_PATH}/sjcl.js
cp ${SJCL_PATH}/sjcl.js ./src/sjcl/index.js
patch src/sjcl/index.d.ts sjcl.point.patch

clean:
rm -f src/sjcl/index.js src/sjcl/index.d.ts
Loading