Skip to content

Commit

Permalink
Implementing RSA blind signatures.
Browse files Browse the repository at this point in the history
  • Loading branch information
armfazh committed May 24, 2022
1 parent dc32d71 commit da52ce5
Show file tree
Hide file tree
Showing 17 changed files with 8,865 additions and 92 deletions.
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

0 comments on commit da52ce5

Please sign in to comment.