-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
8,865 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.