Skip to content

Commit da52ce5

Browse files
committed
Implementing RSA blind signatures.
1 parent dc32d71 commit da52ce5

17 files changed

+8865
-92
lines changed

.eslintrc.json

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
"plugin:prettier/recommended",
2121
"prettier"
2222
],
23+
"ignorePatterns": [
24+
"**/*.d.ts",
25+
"**/*.js",
26+
"coverage/*",
27+
"lib/*"
28+
],
2329
"rules": {
2430
"@typescript-eslint/member-delimiter-style": 0,
2531
"@typescript-eslint/no-namespace": [

package-lock.json

+93-90
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build": "webpack",
1818
"pretest": "npm run sjcl",
1919
"test": "tsc -b && node --experimental-vm-modules node_modules/jest/bin/jest.js --ci",
20-
"lint": "eslint ./src/**/*.{ts,tsx}",
20+
"lint": "eslint .",
2121
"clean": "rimraf dist"
2222
},
2323
"dependencies": {

src/blindrsa/blindrsa.test.ts

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import blindRSA from './index';
2+
import { jest } from '@jest/globals';
3+
import sjcl from './sjcl';
4+
// Test vector
5+
// https://www.ietf.org/archive/id/draft-irtf-cfrg-rsa-blind-signatures-03.html#appendix-A
6+
import vectors from './testdata/rsablind_vectors.json';
7+
8+
function hexToB64URL(x: string): string {
9+
return Buffer.from(x, 'hex').toString('base64url');
10+
}
11+
12+
function hexToUint8(x: string): Uint8Array {
13+
return new Uint8Array(Buffer.from(x, 'hex'));
14+
}
15+
16+
function paramsFromVector(v: typeof vectors[number]): {
17+
n: string;
18+
e: string;
19+
d: string;
20+
p: string;
21+
q: string;
22+
dp: string;
23+
dq: string;
24+
qi: string;
25+
} {
26+
const n = hexToB64URL(v.n);
27+
const e = hexToB64URL(v.e);
28+
const d = hexToB64URL(v.d);
29+
const p = hexToB64URL(v.p);
30+
const q = hexToB64URL(v.q);
31+
32+
// Calculate CRT values
33+
const bnD = new sjcl.bn(v.d);
34+
const bnP = new sjcl.bn(v.p);
35+
const bnQ = new sjcl.bn(v.q);
36+
const one = new sjcl.bn(1);
37+
const dp = hexToB64URL(bnD.mod(bnP.sub(one)).toString());
38+
const dq = hexToB64URL(bnD.mod(bnQ.sub(one)).toString());
39+
const qi = hexToB64URL(bnQ.inverseMod(bnP).toString());
40+
return { n, e, d, p, q, dp, dq, qi };
41+
}
42+
43+
async function keysFromVector(
44+
v: typeof vectors[number],
45+
extractable: boolean,
46+
): Promise<CryptoKeyPair> {
47+
const params = paramsFromVector(v);
48+
const { n, e } = params;
49+
const publicKey = await crypto.subtle.importKey(
50+
'jwk',
51+
{ kty: 'RSA', ext: true, n, e },
52+
{ name: 'RSA-PSS', hash: 'SHA-384' },
53+
extractable,
54+
['verify'],
55+
);
56+
57+
const privateKey = await crypto.subtle.importKey(
58+
'jwk',
59+
{ kty: 'RSA', ext: true, ...params },
60+
{ name: 'RSA-PSS', hash: 'SHA-384' },
61+
extractable,
62+
['sign'],
63+
);
64+
return { privateKey, publicKey };
65+
}
66+
67+
describe.each(vectors)('BlindRSA-vec$#', (v: typeof vectors[number]) => {
68+
test('test-vector', async () => {
69+
const r_inv = new sjcl.bn(v.inv);
70+
const r = r_inv.inverseMod(new sjcl.bn(v.n));
71+
const r_bytes = hexToUint8(r.toString().slice(2));
72+
73+
const { privateKey, publicKey } = await keysFromVector(v, true);
74+
const msg = hexToUint8(v.msg);
75+
const saltLength = v.salt.length / 2;
76+
77+
// Mock for randomized blind operation.
78+
jest.spyOn(crypto, 'getRandomValues')
79+
.mockReturnValueOnce(hexToUint8(v.salt)) // mock for random salt
80+
.mockReturnValueOnce(r_bytes); // mock for random blind
81+
82+
const { blindedMsg, blindInv } = await blindRSA.blind(publicKey, msg, saltLength);
83+
expect(blindedMsg).toStrictEqual(hexToUint8(v.blinded_msg));
84+
expect(blindInv).toStrictEqual(hexToUint8(v.inv));
85+
86+
const blindedSig = await blindRSA.blindSign(privateKey, blindedMsg);
87+
expect(blindedSig).toStrictEqual(hexToUint8(v.blind_sig));
88+
89+
const signature = await blindRSA.finalize(publicKey, msg, blindInv, blindedSig, saltLength);
90+
expect(signature).toStrictEqual(hexToUint8(v.sig));
91+
});
92+
93+
test('non-extractable-keys', async () => {
94+
const { privateKey, publicKey } = await keysFromVector(v, false);
95+
const msg = crypto.getRandomValues(new Uint8Array(10));
96+
const blindedMsg = crypto.getRandomValues(new Uint8Array(32));
97+
const blindInv = crypto.getRandomValues(new Uint8Array(32));
98+
const blindedSig = crypto.getRandomValues(new Uint8Array(32));
99+
const errorMsg = 'key is not extractable';
100+
101+
await expect(blindRSA.blind(publicKey, msg, 32)).rejects.toThrow(errorMsg);
102+
await expect(blindRSA.blindSign(privateKey, blindedMsg)).rejects.toThrow(errorMsg);
103+
await expect(blindRSA.finalize(publicKey, msg, blindInv, blindedSig, 32)).rejects.toThrow(
104+
errorMsg,
105+
);
106+
});
107+
108+
test('wrong-key-type', async () => {
109+
const { privateKey, publicKey } = await crypto.subtle.generateKey(
110+
{
111+
name: 'RSASSA-PKCS1-v1_5', // not RSA-PSS.
112+
modulusLength: 2048,
113+
publicExponent: Uint8Array.from([0x01, 0x00, 0x01]),
114+
hash: 'SHA-256',
115+
},
116+
true,
117+
['sign', 'verify'],
118+
);
119+
120+
const msg = crypto.getRandomValues(new Uint8Array(10));
121+
const blindedMsg = crypto.getRandomValues(new Uint8Array(32));
122+
const blindInv = crypto.getRandomValues(new Uint8Array(32));
123+
const blindedSig = crypto.getRandomValues(new Uint8Array(32));
124+
const errorMsg = 'key is not RSA-PSS';
125+
126+
await expect(blindRSA.blind(publicKey, msg, 32)).rejects.toThrow(errorMsg);
127+
await expect(blindRSA.blindSign(privateKey, blindedMsg)).rejects.toThrow(errorMsg);
128+
await expect(blindRSA.finalize(publicKey, msg, blindInv, blindedSig, 32)).rejects.toThrow(
129+
errorMsg,
130+
);
131+
});
132+
});

src/blindrsa/blindrsa.ts

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { emsa_pss_encode, i2osp, os2ip, rsasp1, rsavp1 } from './util';
2+
3+
import sjcl from './sjcl';
4+
5+
export async function blind(
6+
publicKey: CryptoKey,
7+
msg: Uint8Array,
8+
saltLength = 0,
9+
): Promise<{
10+
blindedMsg: Uint8Array;
11+
blindInv: Uint8Array;
12+
}> {
13+
if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'RSA-PSS') {
14+
throw new Error('key is not RSA-PSS');
15+
}
16+
if (!publicKey.extractable) {
17+
throw new Error('key is not extractable');
18+
}
19+
20+
const { modulusLength, hash: hashFn } = publicKey.algorithm as RsaHashedKeyGenParams;
21+
const kBits = modulusLength;
22+
const kLen = Math.ceil(kBits / 8);
23+
const hash = (hashFn as Algorithm).name;
24+
25+
// 1. encoded_msg = EMSA-PSS-ENCODE(msg, kBits - 1)
26+
// with MGF and HF as defined in the parameters
27+
// 2. If EMSA-PSS-ENCODE raises an error, raise the error and stop
28+
const encoded_msg = await emsa_pss_encode(msg, kBits - 1, { sLen: saltLength, hash });
29+
30+
// 3. m = bytes_to_int(encoded_msg)
31+
const m = os2ip(encoded_msg);
32+
const jwkKey = await crypto.subtle.exportKey('jwk', publicKey);
33+
if (!jwkKey.n || !jwkKey.e) {
34+
throw new Error('key has invalid parameters');
35+
}
36+
const n = new sjcl.bn(Buffer.from(jwkKey.n, 'base64url').toString('hex'));
37+
const e = new sjcl.bn(Buffer.from(jwkKey.e, 'base64url').toString('hex'));
38+
39+
// 4. r = random_integer_uniform(1, n)
40+
let r: sjcl.bn;
41+
do {
42+
r = os2ip(crypto.getRandomValues(new Uint8Array(kLen)));
43+
} while (r.greaterEquals(n));
44+
45+
// 5. r_inv = inverse_mod(r, n)
46+
// 6. If inverse_mod fails, raise an "invalid blind" error
47+
// and stop
48+
let r_inv: sjcl.bn;
49+
try {
50+
r_inv = r.inverseMod(new sjcl.bn(n));
51+
} catch (e) {
52+
throw new Error('invalid blind');
53+
}
54+
// 7. x = RSAVP1(pkS, r)
55+
const x = rsavp1({ n, e }, r);
56+
57+
// 8. z = m * x mod n
58+
const z = m.mulmod(x, n);
59+
60+
// 9. blinded_msg = int_to_bytes(z, kLen)
61+
const blindedMsg = i2osp(z, kLen);
62+
63+
// 10. inv = int_to_bytes(r_inv, kLen)
64+
const blindInv = i2osp(r_inv, kLen);
65+
66+
// 11. output blinded_msg, inv
67+
return { blindedMsg, blindInv };
68+
}
69+
70+
export async function finalize(
71+
publicKey: CryptoKey,
72+
msg: Uint8Array,
73+
blindInv: Uint8Array,
74+
blindSig: Uint8Array,
75+
saltLength = 0,
76+
): Promise<Uint8Array> {
77+
if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'RSA-PSS') {
78+
throw new Error('key is not RSA-PSS');
79+
}
80+
if (!publicKey.extractable) {
81+
throw new Error('key is not extractable');
82+
}
83+
const { modulusLength } = publicKey.algorithm as RsaHashedKeyGenParams;
84+
const kLen = Math.ceil(modulusLength / 8);
85+
86+
// 1. If len(blind_sig) != kLen, raise "unexpected input size" and stop
87+
// 2. If len(inv) != kLen, raise "unexpected input size" and stop
88+
if (blindSig.length != kLen || blindInv.length != kLen) {
89+
throw new Error('unexpected input size');
90+
}
91+
92+
// 3. z = bytes_to_int(blind_sig)
93+
const z = os2ip(blindSig);
94+
95+
// 4. r_inv = bytes_to_int(inv)
96+
const r_inv = os2ip(blindInv);
97+
98+
// 5. s = z * r_inv mod n
99+
const jwkKey = await crypto.subtle.exportKey('jwk', publicKey);
100+
if (!jwkKey.n) {
101+
throw new Error('key has invalid parameters');
102+
}
103+
const n = new sjcl.bn(Buffer.from(jwkKey.n, 'base64url').toString('hex'));
104+
const s = z.mulmod(r_inv, n);
105+
106+
// 6. sig = int_to_bytes(s, kLen)
107+
const sig = i2osp(s, kLen);
108+
109+
// 7. result = RSASSA-PSS-VERIFY(pkS, msg, sig)
110+
// 8. If result = "valid signature", output sig, else
111+
// raise "invalid signature" and stop
112+
const algorithm = { name: 'RSA-PSS', saltLength };
113+
if (!(await crypto.subtle.verify(algorithm, publicKey, sig, msg))) {
114+
throw new Error('invalid signature');
115+
}
116+
117+
return sig;
118+
}
119+
120+
export async function blindSign(privateKey: CryptoKey, blindMsg: Uint8Array): Promise<Uint8Array> {
121+
if (privateKey.type !== 'private' || privateKey.algorithm.name !== 'RSA-PSS') {
122+
throw new Error('key is not RSA-PSS');
123+
}
124+
if (!privateKey.extractable) {
125+
throw new Error('key is not extractable');
126+
}
127+
const { modulusLength } = privateKey.algorithm as RsaHashedKeyGenParams;
128+
const kLen = Math.ceil(modulusLength / 8);
129+
130+
// 1. If len(blinded_msg) != kLen, raise "unexpected input size"
131+
// and stop
132+
if (blindMsg.length != kLen) {
133+
throw new Error('unexpected input size');
134+
}
135+
136+
// 2. m = bytes_to_int(blinded_msg)
137+
const m = os2ip(blindMsg);
138+
139+
// 3. If m >= n, raise "invalid message length" and stop
140+
const jwkKey = await crypto.subtle.exportKey('jwk', privateKey);
141+
if (!jwkKey.n || !jwkKey.d) {
142+
throw new Error('key is not a private key');
143+
}
144+
const n = new sjcl.bn(Buffer.from(jwkKey.n, 'base64url').toString('hex'));
145+
const d = new sjcl.bn(Buffer.from(jwkKey.d, 'base64url').toString('hex'));
146+
if (m.greaterEquals(n)) {
147+
throw new Error('invalid message length');
148+
}
149+
150+
// 4. s = RSASP1(skS, m)
151+
const s = rsasp1({ n, d }, m);
152+
153+
// 5. blind_sig = int_to_bytes(s, kLen)
154+
// 6. output blind_sig
155+
return i2osp(s, kLen);
156+
}

src/blindrsa/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { blind, blindSign, finalize } from './blindrsa';
2+
export default { blindSign, finalize, blind };
3+
export { blind, blindSign, finalize } from './blindrsa';

src/blindrsa/sjcl.Makefile

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
SJCL_PATH=node_modules/sjcl
2+
3+
all:
4+
cd ${SJCL_PATH} && \
5+
./configure --without-all --with-ecc --with-convenience --compress=none \
6+
--with-codecBytes --with-codecHex --with-codecArrayBuffer && \
7+
make
8+
npm i -D dts-gen
9+
npx dts-gen -m sjcl -o -f ./src/sjcl/index
10+
npm un -D dts-gen
11+
echo "export default sjcl;" >> ${SJCL_PATH}/sjcl.js
12+
cp ${SJCL_PATH}/sjcl.js ./src/sjcl/index.js
13+
patch src/sjcl/index.d.ts sjcl.point.patch
14+
15+
clean:
16+
rm -f src/sjcl/index.js src/sjcl/index.d.ts

0 commit comments

Comments
 (0)