mod common; #[test] fn webcrypto_algorithm_conversion_and_key_policy_are_preserved() { let out = common::run( r#" let reads = 0; const algorithm = { get name() { reads++; return 'AES-GCM'; }, length: 128, }; const key = await crypto.subtle.generateKey(algorithm, false, ['encrypt']); if (reads !== 1) throw Error('Algorithm.name getter read more than once'); try { await crypto.subtle.exportKey('raw', key); throw Error('Non-extractable key exported'); } catch (error) { if (!String(error.message).includes('not extractable')) throw error; } console.log('crypto algorithm conversion and key policy'); "#, &[], ); assert_eq!(out.stdout, b"crypto algorithm conversion and key policy\n"); } #[test] fn web_crypto_random_and_uuid() { let out = common::run( r#" if (!(crypto instanceof Crypto) || !(crypto.subtle instanceof SubtleCrypto)) throw Error('Interfaces'); for (const value of [new Uint8Array(8), new Uint16Array(4), new BigInt64Array(2), new Uint8ClampedArray(5)]) { if (crypto.getRandomValues(value) !== value) throw Error('Return identity'); } for (const value of [new Float32Array(1), new Float64Array(1), new DataView(new ArrayBuffer(1))]) { try { crypto.getRandomValues(value); throw Error('Accepted invalid view'); } catch (error) { if (error.name !== 'TypeMismatchError') throw error; } } for (const value of [[], null, new Proxy(new Uint8Array(1), {})]) { try { crypto.getRandomValues(value); throw Error('Accepted invalid view'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } for (const length of [65537, 65538]) { try { crypto.getRandomValues(new Uint8Array(length)); throw Error('Accepted oversized view'); } catch (error) { if (error.name !== 'QuotaExceededError') throw error; } } const shared = new Uint8Array(new SharedArrayBuffer(32)); try { crypto.getRandomValues(shared); throw Error('Shared buffer accepted'); } catch(error) { if (!(error instanceof TypeError)) throw error; } for (const buffer of [ new ArrayBuffer(40), new ArrayBuffer(40, {maxByteLength: 80}), ]) { const storage = new Uint8Array(buffer).fill(99); const view = new Uint16Array(buffer, 4, 12); if (crypto.getRandomValues(view) !== view || storage.slice(0, 4).some(byte => byte !== 99) || storage.slice(28).some(byte => byte !== 99)) throw Error('Random view bounds'); } const ids = new Set(Array.from({length: 64}, () => crypto.randomUUID())); if (ids.size !== 64 || [...ids].some(id => !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id))) throw Error('UUID v4'); console.log('crypto-random'); "#, &[], ); assert_eq!(out.stdout, b"crypto-random\n"); } #[test] fn jwk_dictionaries_snapshot_known_members_without_json_or_private_iterator_hooks() { let out = common::run( r#" const subtle = crypto.subtle, sign = new Set(['sign']), signVerify = new Set(['sign','verify']); let reads = 0, iteratorReads = 0; const input = Object.create({ kty: 'oct', get k() {reads++; return 'AQIDBA';}, ext: true, key_ops: { get [Symbol.iterator]() { iteratorReads++; return function() {return sign.values();}; }, }, toJSON() {throw Error('JWK toJSON');}, get unknown() {throw Error('Unknown JWK field');}, }); const imported = subtle.importKey('jwk', input, {name:'HMAC', hash:'SHA-256'}, true, sign); Object.defineProperty(input, 'k', {value: 'BQYHCA'}); const key = await imported; const bytes = new Uint8Array(await subtle.exportKey('raw', key)); if (reads !== 1 || iteratorReads !== 1 || bytes.join() !== '1,2,3,4') throw Error('JWK snapshot'); const pair = await subtle.generateKey('Ed25519', true, signVerify); const privateJwk = await subtle.exportKey('jwk', pair.privateKey); privateJwk.toJSON = () => {throw Error('Asymmetric JWK toJSON');}; privateJwk.unknown = privateJwk; privateJwk.key_ops = sign; const oldIterator = Array.prototype[Symbol.iterator]; const oldParse = JSON.parse, oldStringify = JSON.stringify; try { Array.prototype[Symbol.iterator] = JSON.parse = JSON.stringify = () => {throw Error('Guest hook');}; const copy = await subtle.importKey('jwk', privateJwk, 'Ed25519', true, sign); const exported = await subtle.exportKey('jwk', copy); if (exported.d !== privateJwk.d || exported.kty !== 'OKP') throw Error('JWK round trip'); const generated = await subtle.generateKey('Ed25519', true, signVerify); if (generated.privateKey.type !== 'private') throw Error('Key metadata iterator'); } finally { Array.prototype[Symbol.iterator] = oldIterator; JSON.parse = oldParse; JSON.stringify = oldStringify; } const reason = {}; try { await subtle.importKey('jwk', {get k() {throw reason;}}, 'AES-GCM', false, new Set(['encrypt'])); throw Error('Lost conversion failure'); } catch (error) {if (error !== reason) throw error;} for (const key_ops of [null, {}, 1]) { try { await subtle.importKey('jwk', {kty:'oct',k:'AQIDBA',key_ops}, {name:'HMAC',hash:'SHA-256'}, false, sign); throw Error('Accepted non-iterable key_ops'); } catch (error) {if (!(error instanceof TypeError)) throw error;} } console.log('jwk dictionaries'); "#, &[], ); assert_eq!(out.stdout, b"jwk dictionaries\n"); } #[test] fn web_crypto_symmetric_algorithms_and_keys() { let out = common::run( r#" const bytes = hex => Uint8Array.from(hex.match(/../g).map(x => parseInt(x, 16))); const hex = value => [...new Uint8Array(value)].map(x => x.toString(16).padStart(2, '0')).join(''); const eq = (a,b,label) => { if (a !== b) throw Error(label + ': ' + a); }; const subtle = crypto.subtle; const aesKey = await subtle.importKey('raw', bytes('00000000000000000000000000000000'), 'AES-GCM', false, new Set(['encrypt','decrypt'])); if (aesKey.type !== 'secret' || aesKey.extractable || aesKey.algorithm.name !== 'AES-GCM' || aesKey.algorithm.length !== 128 || !Object.isFrozen(aesKey.usages)) throw Error('CryptoKey metadata'); const algorithm = aesKey.algorithm; algorithm.length = 999; if (aesKey.algorithm !== algorithm || aesKey.algorithm.length !== 999) throw Error('Algorithm caching'); if (structuredClone(aesKey).algorithm.length !== 128) throw Error('Public metadata mutated private key policy'); try { await subtle.exportKey('raw', aesKey); throw Error('Nonextractable export'); } catch (e) { if (e.name !== 'InvalidAccessError') throw e; } const iv = bytes('000000000000000000000000'); const cipher = await subtle.encrypt({name:'AES-GCM',iv}, aesKey, bytes('00000000000000000000000000000000')); eq(hex(cipher), '0388dace60b6a392f328c2b971b2fe78ab6e47d42cec13bdf53a67b21257bddf', 'AES-GCM vector'); eq(hex(await subtle.decrypt({name:'AES-GCM',iv}, aesKey, cipher)), '00000000000000000000000000000000', 'AES-GCM decrypt'); const aes192 = await subtle.importKey('raw', new Uint8Array(24), {name:'AES-GCM'}, true, ['encrypt','decrypt']); for (const tagLength of [32, 64, 96, 104, 112, 120, 128]) { const tagged = await subtle.encrypt({name:'AES-GCM',iv,tagLength}, aes192, bytes('010203')); eq(hex(await subtle.decrypt({name:'AES-GCM',iv,tagLength}, aes192, tagged)), '010203', 'AES-GCM tag ' + tagLength); } for (const tagLength of [0, 31, 33, 48, 129]) { try { await subtle.encrypt({name:'AES-GCM',iv,tagLength}, aes192, bytes('01')); throw Error('Accepted invalid GCM tag length'); } catch (error) { if (error.message === 'Accepted invalid GCM tag length' || error.name !== 'OperationError') throw error; } } try { await subtle.encrypt({name:'AES-GCM',iv:new Uint8Array(0)}, aes192, bytes('01')); throw Error('Accepted empty GCM IV'); } catch (error) { if (error.message === 'Accepted empty GCM IV' || error.name !== 'OperationError') throw error; } const generated = await subtle.generateKey({name:'AES-CTR',length:192}, true, ['encrypt','encrypt','decrypt']); try { await subtle.generateKey({name:'AES-GCM',length:128}, false, {0:'encrypt',length:1}); throw Error('Accepted arraylike usages'); } catch (error) { if (!(error instanceof TypeError)) throw error; } if (generated.algorithm.length !== 192 || generated.usages.join(',') !== 'encrypt,decrypt' || (await subtle.exportKey('raw', generated)).byteLength !== 24) throw Error('Generated AES key'); const generatedHmac = await subtle.generateKey({name:'HMAC',hash:'SHA-256'}, true, ['sign']); if (generatedHmac.algorithm.length !== 512 || (await subtle.exportKey('raw', generatedHmac)).byteLength !== 64) throw Error('Generated HMAC default length'); const cbcKey = await subtle.importKey('raw', bytes('2b7e151628aed2a6abf7158809cf4f3c'), 'AES-CBC', true, ['encrypt','decrypt']); const cbcIv = bytes('000102030405060708090a0b0c0d0e0f'); const cbcCipher = await subtle.encrypt({name:'AES-CBC',iv:cbcIv}, cbcKey, bytes('6bc1bee22e409f96e93d7e117393172a')); eq(hex(await subtle.decrypt({name:'AES-CBC',iv:cbcIv}, cbcKey, cbcCipher)), '6bc1bee22e409f96e93d7e117393172a', 'AES-CBC roundtrip'); const ctrKey = await subtle.importKey('raw', bytes('2b7e151628aed2a6abf7158809cf4f3c'), 'AES-CTR', true, ['encrypt','decrypt']); const ctrCounter = bytes('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); const ctrCipher = await subtle.encrypt({name:'AES-CTR',counter:ctrCounter,length:128}, ctrKey, bytes('6bc1bee22e409f96e93d7e117393172a')); eq(hex(ctrCipher), '874d6191b620e3261bef6864990db6ce', 'AES-CTR vector'); const zeroCtr = await subtle.importKey('raw', new Uint8Array(16), 'AES-CTR', true, ['encrypt','decrypt']); const rollover = await subtle.encrypt({name:'AES-CTR',counter:bytes('000000000000000000000000ffffffff'),length:32}, zeroCtr, new Uint8Array(32)); eq(hex(rollover), '28c16380c491088ca019f8a76853b1e866e94bd4ef8a2c3b884cfa59ca342b2e', 'AES-CTR bounded counter'); try { await subtle.encrypt({name:'AES-CTR',counter:new Uint8Array(16),length:1}, zeroCtr, new Uint8Array(48)); throw Error('Counter space overflow accepted'); } catch (error) { if (error.message === 'Counter space overflow accepted' || error.name !== 'OperationError') throw error; } const unusualIv = bytes('0102030405060708'); const oddNonceCipher = await subtle.encrypt({name:'AES-GCM',iv:unusualIv}, aesKey, bytes('01')); eq(hex(await subtle.decrypt({name:'AES-GCM',iv:unusualIv}, aesKey, oddNonceCipher)), '01', 'AES-GCM non-96-bit IV'); for (const length of [17, 32, 64]) { const nonce = new Uint8Array(length).fill(length); const encrypted = await subtle.encrypt({name:'AES-GCM',iv:nonce}, aesKey, bytes('abcdef')); eq(hex(await subtle.decrypt({name:'AES-GCM',iv:nonce}, aesKey, encrypted)), 'abcdef', 'AES-GCM long IV ' + length); } const hmacKey = await subtle.importKey('raw', new TextEncoder().encode('key'), {name:'HMAC',hash:'SHA-256'}, true, ['sign','verify']); const dataView = new DataView(new Uint8Array([7,97,98,99,8]).buffer, 1, 3); const dataViewDigest = hex(await crypto.subtle.digest('SHA-256', dataView)); if (dataViewDigest !== 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') throw Error('DataView BufferSource: ' + dataViewDigest); const partialHmac = await subtle.importKey('raw', new Uint8Array([255,255]), {name:'HMAC',hash:'SHA-256',length:9}, true, ['sign']); if (partialHmac.algorithm.length !== 9 || [...new Uint8Array(await subtle.exportKey('raw', partialHmac))].join(',') !== '255,128') throw Error('Partial-bit HMAC key'); const partialGenerated = await subtle.generateKey({name:'HMAC',hash:'SHA-256',length:9}, true, ['sign']); if (partialGenerated.algorithm.length !== 9 || (await subtle.exportKey('raw', partialGenerated)).byteLength !== 2) throw Error('Partial-bit HMAC generation'); const message = new TextEncoder().encode('The quick brown fox jumps over the lazy dog'); const signature = await subtle.sign('HMAC', hmacKey, message); eq(hex(signature), 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8', 'HMAC vector'); if (!await subtle.verify('HMAC', hmacKey, signature, message) || await subtle.verify('HMAC', hmacKey, new Uint8Array([0]), message)) throw Error('HMAC verify'); const exported = await subtle.exportKey('raw', hmacKey); const jwk = await subtle.exportKey('jwk', hmacKey); if (jwk.kty !== 'oct' || jwk.alg !== 'HS256' || jwk.ext !== true || !jwk.key_ops.includes('sign')) throw Error('JWK export'); const importedJwk = await subtle.importKey('jwk', jwk, {name:'HMAC',hash:'SHA-256'}, true, ['verify']); if (!await subtle.verify('HMAC', importedJwk, signature, message)) throw Error('JWK import'); for (const badJwk of [{...jwk, alg:'HS512'}, {...jwk, key_ops:['sign','sign']}, {...jwk, key_ops:['execute']}, {...jwk, ext:false}]) { try { await subtle.importKey('jwk', badJwk, {name:'HMAC',hash:'SHA-256'}, true, ['verify']); throw Error('Accepted invalid JWK'); } catch (error) { if (error.message === 'Accepted invalid JWK') throw error; if (error.name !== 'DataError') throw error; } } const importedRaw = await subtle.importKey('raw', exported, {name:'HMAC',hash:'SHA-256'}, true, ['sign']); if (hex(await subtle.sign('HMAC', importedRaw, message)) !== hex(signature)) throw Error('Raw import'); const partialJwk = await subtle.exportKey('jwk', partialHmac); const importedPartialJwk = await subtle.importKey('jwk', partialJwk, {name:'HMAC',hash:'SHA-256',length:9}, true, ['sign']); const partialJwkRaw = [...new Uint8Array(await subtle.exportKey('raw', importedPartialJwk))].join(','); if (importedPartialJwk.algorithm.length !== 9 || partialJwkRaw !== '255,128') throw Error('Partial-bit HMAC JWK ' + importedPartialJwk.algorithm.length + ':' + partialJwkRaw); try { new CryptoKey(); throw Error('Public CryptoKey constructor'); } catch(e) { if (!(e instanceof TypeError)) throw e; } const originals = [Array.prototype.includes, Array.prototype.some, Array.prototype.slice, String.prototype.toLowerCase, String.prototype.startsWith, Number.isInteger]; try { Array.prototype.includes = Array.prototype.some = Array.prototype.slice = () => { throw Error('poisoned array'); }; String.prototype.toLowerCase = String.prototype.startsWith = () => { throw Error('poisoned string'); }; Number.isInteger = () => false; const poisonKey = await subtle.importKey('raw', new Uint8Array(16), 'AES-GCM', false, ['encrypt']); await subtle.encrypt({name:'AES-GCM',iv:new Uint8Array(12)}, poisonKey, new Uint8Array(1)); } finally { [Array.prototype.includes, Array.prototype.some, Array.prototype.slice, String.prototype.toLowerCase, String.prototype.startsWith, Number.isInteger] = originals; } console.log('symmetric'); "#, &[], ); assert_eq!(out.stdout, b"symmetric\n"); } #[test] fn web_crypto_asymmetric_key_generation_and_operations() { let out = common::run( r#" const subtle = crypto.subtle; const message = new TextEncoder().encode('asymmetric vector'); const ecdsa = await subtle.generateKey({name:'ECDSA',namedCurve:'P-256'}, true, ['sign','verify']); if (ecdsa.publicKey.type !== 'public' || ecdsa.privateKey.type !== 'private' || ecdsa.publicKey.extractable !== true || ecdsa.privateKey.extractable !== true || ecdsa.publicKey.algorithm.namedCurve !== 'P-256') throw Error('ECDSA pair metadata'); const sig = await subtle.sign({name:'ECDSA',hash:'SHA-256'}, ecdsa.privateKey, message); if (sig.byteLength !== 64 || !await subtle.verify({name:'ECDSA',hash:'SHA-256'}, ecdsa.publicKey, sig, message)) throw Error('ECDSA P1363'); const publicDer = await subtle.exportKey('spki', ecdsa.publicKey); const publicRaw = await subtle.exportKey('raw', ecdsa.publicKey); const privateDer = await subtle.exportKey('pkcs8', ecdsa.privateKey); const importedPublic = await subtle.importKey('spki', publicDer, {name:'ECDSA',namedCurve:'P-256'}, false, ['verify']); const importedRaw = await subtle.importKey('raw', publicRaw, {name:'ECDSA',namedCurve:'P-256'}, true, ['verify']); const importedPrivate = await subtle.importKey('pkcs8', privateDer, {name:'ECDSA',namedCurve:'P-256'}, false, ['sign']); if (importedPublic.extractable || !await subtle.verify({name:'ECDSA',hash:'SHA-256'}, importedPublic, sig, message) || !await subtle.verify({name:'ECDSA',hash:'SHA-256'}, importedRaw, sig, message)) throw Error('ECDSA public import/export'); if (await subtle.exportKey('pkcs8', importedPrivate).then(() => true, () => false)) throw Error('ECDSA private became extractable'); const ecdhA = await subtle.generateKey({name:'ECDH',namedCurve:'P-256'}, false, ['deriveBits','deriveKey']); const ecdhB = await subtle.generateKey({name:'ECDH',namedCurve:'P-256'}, false, ['deriveBits','deriveKey']); const secretA = await subtle.deriveBits({name:'ECDH',public:ecdhB.publicKey}, ecdhA.privateKey, 255); const secretB = await subtle.deriveBits({name:'ECDH',public:ecdhA.publicKey}, ecdhB.privateKey, 255); if (new Uint8Array(secretA).some((b, i) => b !== new Uint8Array(secretB)[i])) throw Error('ECDH'); const derived = await subtle.deriveKey({name:'ECDH',public:ecdhB.publicKey}, ecdhA.privateKey, {name:'AES-GCM',length:128}, true, ['encrypt']); if ((await subtle.exportKey('raw', derived)).byteLength !== 16) throw Error('ECDH deriveKey'); const xA = await subtle.generateKey('X25519', true, ['deriveBits']); const xB = await subtle.generateKey('X25519', false, ['deriveBits']); const xSecret = await subtle.deriveBits({name:'X25519',public:xB.publicKey}, xA.privateKey, 256); const xSecretB = await subtle.deriveBits({name:'X25519',public:xA.publicKey}, xB.privateKey, 256); if (new Uint8Array(xSecret).some((b, i) => b !== new Uint8Array(xSecretB)[i])) throw Error('X25519'); const xPrivate = await subtle.exportKey('pkcs8', xA.privateKey); const xPublic = await subtle.exportKey('spki', xB.publicKey); const xImportedPrivate = await subtle.importKey('pkcs8', xPrivate, 'X25519', false, ['deriveBits']); const xImportedPublic = await subtle.importKey('spki', xPublic, 'X25519', false, []); if (new Uint8Array(await subtle.deriveBits({name:'X25519',public:xImportedPublic}, xImportedPrivate, 256)).some((b, i) => b !== new Uint8Array(xSecret)[i])) throw Error('X25519 DER'); const xJwk = await subtle.exportKey('jwk', xA.privateKey); const xJwkPrivate = await subtle.importKey('jwk', xJwk, 'X25519', false, ['deriveBits']); if (new Uint8Array(await subtle.deriveBits({name:'X25519',public:xB.publicKey}, xJwkPrivate, 256)).some((b, i) => b !== new Uint8Array(xSecret)[i])) throw Error('X25519 JWK'); for (const curve of ['P-384','P-521']) { const pair = await subtle.generateKey({name:'ECDSA',namedCurve:curve}, true, ['sign','verify']); const hash = curve === 'P-384' ? 'SHA-384' : 'SHA-512'; const signature = await subtle.sign({name:'ECDSA',hash}, pair.privateKey, message); if (!await subtle.verify({name:'ECDSA',hash}, pair.publicKey, signature, message)) throw Error(curve+' ECDSA'); const imported = await subtle.importKey('spki', await subtle.exportKey('spki', pair.publicKey), {name:'ECDSA',namedCurve:curve}, false, ['verify']); if (!await subtle.verify({name:'ECDSA',hash}, imported, signature, message)) throw Error(curve+' DER'); const alice = await subtle.generateKey({name:'ECDH',namedCurve:curve}, false, ['deriveBits']); const bob = await subtle.generateKey({name:'ECDH',namedCurve:curve}, false, ['deriveBits']); const sharedA = await subtle.deriveBits({name:'ECDH',public:bob.publicKey}, alice.privateKey, pair.publicKey.algorithm.length); const sharedB = await subtle.deriveBits({name:'ECDH',public:alice.publicKey}, bob.privateKey, pair.publicKey.algorithm.length); if (new Uint8Array(sharedA).some((b, i) => b !== new Uint8Array(sharedB)[i])) throw Error(curve+' ECDH'); } const ed = await subtle.generateKey('Ed25519', true, ['sign','verify']); const edSig = await subtle.sign('Ed25519', ed.privateKey, message); if (ed.publicKey.algorithm.name !== 'Ed25519' || edSig.byteLength !== 64 || !await subtle.verify('Ed25519', ed.publicKey, edSig, message)) throw Error('Ed25519'); const edJwk = await subtle.exportKey('jwk', ed.privateKey); const edImported = await subtle.importKey('jwk', edJwk, 'Ed25519', false, ['sign']); const edPrivateJwk = await subtle.importKey('jwk', edJwk, 'Ed25519', true, ['sign']); if (edImported.extractable || !await subtle.verify('Ed25519', ed.publicKey, await subtle.sign('Ed25519', edPrivateJwk, message), message)) throw Error('Ed25519 JWK'); const ecJwk = await subtle.exportKey('jwk', ecdsa.privateKey); const ecPrivateJwk = await subtle.importKey('jwk', ecJwk, {name:'ECDSA',namedCurve:'P-256'}, true, ['sign']); const ecPublicJwk = await subtle.importKey('jwk', await subtle.exportKey('jwk', ecdsa.publicKey), {name:'ECDSA',namedCurve:'P-256'}, true, ['verify']); if (!await subtle.verify({name:'ECDSA',hash:'SHA-256'}, ecPublicJwk, await subtle.sign({name:'ECDSA',hash:'SHA-256'}, ecPrivateJwk, message), message)) throw Error('ECDSA JWK'); const wrapping = await subtle.generateKey({name:'AES-KW',length:128}, false, ['wrapKey','unwrapKey']); const wrappedJwk = await subtle.wrapKey('jwk', ecdsa.privateKey, wrapping, 'AES-KW'); const unwrappedJwk = await subtle.unwrapKey('jwk', wrappedJwk, wrapping, 'AES-KW', {name:'ECDSA',namedCurve:'P-256'}, false, ['sign']); if (unwrappedJwk.extractable || !await subtle.verify({name:'ECDSA',hash:'SHA-256'}, ecdsa.publicKey, await subtle.sign({name:'ECDSA',hash:'SHA-256'}, unwrappedJwk, message), message)) throw Error('asymmetric JWK unwrap'); const edWrappingPair = await subtle.generateKey('Ed25519', true, ['sign','verify']); const wrappedPublic = await subtle.wrapKey('raw', edWrappingPair.publicKey, wrapping, 'AES-KW'); const unwrappedPublic = await subtle.unwrapKey('raw', wrappedPublic, wrapping, 'AES-KW', 'Ed25519', false, ['verify']); const edPublicSignature = await subtle.sign('Ed25519', edWrappingPair.privateKey, message); if (unwrappedPublic.type !== 'public' || unwrappedPublic.extractable || !await subtle.verify('Ed25519', unwrappedPublic, edPublicSignature, message)) throw Error('asymmetric public unwrap'); const rsa = await subtle.generateKey({name:'RSA-PSS',modulusLength:1024,publicExponent:new Uint8Array([1,0,1]),hash:'SHA-256'}, true, ['sign','verify']); if (rsa.publicKey.algorithm.modulusLength !== 1024 || rsa.privateKey.algorithm.hash.name !== 'SHA-256') throw Error('RSA metadata'); const rsaSig = await subtle.sign({name:'RSA-PSS',saltLength:32}, rsa.privateKey, message); if (!await subtle.verify({name:'RSA-PSS',saltLength:32}, rsa.publicKey, rsaSig, message)) throw Error('RSA-PSS'); const rsaPrivateJwk = await subtle.exportKey('jwk', rsa.privateKey); const importedRsa = await subtle.importKey('jwk', rsaPrivateJwk, {name:'RSA-PSS',hash:'SHA-256'}, false, ['sign']); if (importedRsa.extractable || !await subtle.verify({name:'RSA-PSS',saltLength:32}, rsa.publicKey, await subtle.sign({name:'RSA-PSS',saltLength:32}, importedRsa, message), message)) throw Error('RSA JWK'); const rsaV15 = await subtle.generateKey({name:'RSASSA-PKCS1-V1_5',modulusLength:1024,publicExponent:new Uint8Array([1,0,1]),hash:'SHA-256'}, false, ['sign','verify']); if (rsaV15.publicKey.algorithm.name !== 'RSASSA-PKCS1-v1_5' || rsaV15.publicKey.algorithm.modulusLength !== 1024) throw Error('RSASSA algorithm metadata'); const rsaV15Signature = await subtle.sign('RSASSA-PKCS1-V1_5', rsaV15.privateKey, message); if (!await subtle.verify('RSASSA-PKCS1-V1_5', rsaV15.publicKey, rsaV15Signature, message)) throw Error('RSASSA-PKCS1-v1_5'); const extraHashSignature = await subtle.sign({name:'RSA-PSS',get hash(){throw Error('Read ignored RSA hash');},saltLength:32}, rsa.privateKey, message); if (!await subtle.verify({name:'RSA-PSS',saltLength:32}, rsa.publicKey, extraHashSignature, message)) throw Error('RSA key hash ignored'); if (await subtle.verify({name:'ECDSA',hash:'SHA-256'}, ecdsa.publicKey, new Uint8Array(63), message)) throw Error('Accepted malformed ECDSA signature'); const oaep = await subtle.generateKey({name:'RSA-OAEP',modulusLength:1024,publicExponent:new Uint8Array([1,0,1]),hash:'SHA-256'}, false, ['encrypt','decrypt']); const ciphertext = await subtle.encrypt('RSA-OAEP', oaep.publicKey, message); const label = new TextEncoder().encode('oaep label'); const labeledCiphertext = await subtle.encrypt({name:'RSA-OAEP',label}, oaep.publicKey, message); if (new Uint8Array(await subtle.decrypt({name:'RSA-OAEP',label}, oaep.privateKey, labeledCiphertext)).some((b, i) => b !== message[i])) throw Error('RSA-OAEP label'); const binaryLabel = new Uint8Array([0,255,128,1]); const binaryLabelCiphertext = await subtle.encrypt({name:'RSA-OAEP',label:binaryLabel}, oaep.publicKey, message); if (new Uint8Array(await subtle.decrypt({name:'RSA-OAEP',label:binaryLabel}, oaep.privateKey, binaryLabelCiphertext)).some((b, i) => b !== message[i])) throw Error('RSA-OAEP binary label'); const cleartext = await subtle.decrypt('RSA-OAEP', oaep.privateKey, ciphertext); if (new TextDecoder().decode(cleartext) !== 'asymmetric vector') throw Error('RSA-OAEP'); console.log('crypto-asymmetric'); "#, &[], ); assert_eq!(out.stdout, b"crypto-asymmetric\n"); } #[test] fn web_crypto_kdf_kw_and_clone_policy() { let out = common::run( r#" const bytes = hex => Uint8Array.from(hex.match(/../g).map(x => parseInt(x, 16))); const hex = value => [...new Uint8Array(value)].map(x => x.toString(16).padStart(2, '0')).join(''); const subtle = crypto.subtle; const pb = await subtle.importKey('raw', new TextEncoder().encode('password'), 'PBKDF2', false, ['deriveBits','deriveKey']); if ('length' in pb.algorithm || 'hash' in pb.algorithm) throw Error('PBKDF2 key metadata'); const emptyPassword = await subtle.importKey('raw', new Uint8Array(0), 'PBKDF2', false, ['deriveBits']); if ((await subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt:new Uint8Array(0),iterations:1}, emptyPassword, 128)).byteLength !== 16) throw Error('Empty PBKDF2 password'); const pbBits = await subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt:new TextEncoder().encode('salt'),iterations:1}, pb, 256); if (hex(pbBits) !== '120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b') throw Error('PBKDF2 vector'); const pbSha1 = await subtle.deriveBits({name:'PBKDF2',hash:'SHA-1',salt:new TextEncoder().encode('salt'),iterations:1}, pb, 160); if (hex(pbSha1) !== '0c60c80f961f0e71f3a9b524af6012062fe037a6') throw Error('PBKDF2 SHA-1 vector'); const hk = await subtle.importKey('raw', bytes('0b'.repeat(22)), 'HKDF', false, ['deriveBits','deriveKey']); if ('length' in hk.algorithm || 'hash' in hk.algorithm) throw Error('HKDF key metadata'); const hkBits = await subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:bytes('000102030405060708090a0b0c'),info:bytes('f0f1f2f3f4f5f6f7f8f9')}, hk, 336); if (hex(hkBits) !== '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865') throw Error('HKDF vector'); const hkSha1Key = await subtle.importKey('raw', bytes('0b'.repeat(11)), 'HKDF', false, ['deriveBits']); const hkSha1 = await subtle.deriveBits({name:'HKDF',hash:'SHA-1',salt:bytes('000102030405060708090a0b0c'),info:bytes('f0f1f2f3f4f5f6f7f8f9')}, hkSha1Key, 336); if (hex(hkSha1) !== '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896') throw Error('HKDF SHA-1 vector'); const derived = await subtle.deriveKey({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:new Uint8Array(0)}, hk, {name:'AES-GCM',length:128}, false, ['encrypt','decrypt']); try { await subtle.deriveKey({name:'HKDF',hash:'SHA-256',salt:new Uint8Array(0),info:new Uint8Array(0)}, hk, {name:'HMAC',hash:'SHA-256',length:9}, true, ['sign']); throw Error('Fractional HKDF output accepted'); } catch(e) { if (e.name !== 'OperationError') throw e; } if (!derived || derived.algorithm.length !== 128 || derived.extractable) throw Error('deriveKey metadata'); const kek = await subtle.importKey('raw', bytes('000102030405060708090a0b0c0d0e0f'), 'AES-KW', true, ['wrapKey','unwrapKey']); const rawKey = await subtle.importKey('raw', bytes('00112233445566778899aabbccddeeff'), {name:'AES-GCM'}, true, ['encrypt']); const wrapped = await subtle.wrapKey('raw', rawKey, kek, 'AES-KW'); if (hex(wrapped) !== '1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5') throw Error('AES-KW vector'); const unwrapped = await subtle.unwrapKey('raw', wrapped, kek, 'AES-KW', {name:'AES-GCM'}, true, ['encrypt']); if (hex(await subtle.exportKey('raw', unwrapped)) !== '00112233445566778899aabbccddeeff') throw Error('unwrapKey'); try { await subtle.unwrapKey('raw', new Uint8Array(8), kek, 'AES-KW', {name:'AES-GCM'}, true, ['encrypt']); throw Error('Accepted malformed wrapped key'); } catch (error) { if (error.message === 'Accepted malformed wrapped key' || error.name !== 'OperationError') throw error; } const wrappedJwk = await subtle.wrapKey('jwk', rawKey, kek, 'AES-KW'); const unwrappedJwk = await subtle.unwrapKey('jwk', wrappedJwk, kek, 'AES-KW', {name:'AES-GCM'}, true, ['encrypt']); if (hex(await subtle.exportKey('raw', unwrappedJwk)) !== '00112233445566778899aabbccddeeff') throw Error('JWK wrapping'); const wrappingModes = [ [await subtle.generateKey({name:'AES-CBC',length:128}, false, ['wrapKey','unwrapKey']), {name:'AES-CBC',iv:new Uint8Array(16)}], [await subtle.generateKey({name:'AES-CTR',length:128}, false, ['wrapKey','unwrapKey']), {name:'AES-CTR',counter:new Uint8Array(16),length:128}], [await subtle.generateKey({name:'AES-GCM',length:128}, false, ['wrapKey','unwrapKey']), {name:'AES-GCM',iv:new Uint8Array(12),additionalData:new Uint8Array([1,2])}], ]; for (const [wrappingKey, params] of wrappingModes) { const wrappedBytes = await subtle.wrapKey('raw', rawKey, wrappingKey, params); const unwrappedBytes = await subtle.unwrapKey('raw', wrappedBytes, wrappingKey, params, {name:'AES-GCM'}, false, ['encrypt']); if (unwrappedBytes.extractable) throw Error('Nonextractable unwrap'); try { await subtle.exportKey('raw', unwrappedBytes); throw Error('Unwrapped key became extractable'); } catch (error) { if (error.name !== 'InvalidAccessError') throw error; } const probeIv = new Uint8Array(12); const originalProbe = await subtle.encrypt({name:'AES-GCM',iv:probeIv}, rawKey, new Uint8Array([7,8,9])); const importedProbe = await subtle.encrypt({name:'AES-GCM',iv:probeIv}, unwrappedBytes, new Uint8Array([7,8,9])); if (hex(originalProbe) !== hex(importedProbe)) throw Error('Wrapped key material mismatch'); } const rsaWrap = await subtle.generateKey({name:'RSA-OAEP',modulusLength:1024,publicExponent:new Uint8Array([1,0,1]),hash:'SHA-256'}, false, ['wrapKey','unwrapKey']); const rsaWrapped = await subtle.wrapKey('raw', rawKey, rsaWrap.publicKey, {name:'RSA-OAEP',label:new Uint8Array([4,5])}); const rsaUnwrapped = await subtle.unwrapKey('raw', rsaWrapped, rsaWrap.privateKey, {name:'RSA-OAEP',label:new Uint8Array([4,5])}, {name:'AES-GCM'}, false, ['encrypt']); if (rsaUnwrapped.extractable || rsaUnwrapped.algorithm.length !== 128) throw Error('RSA-OAEP unwrap'); try { await subtle.unwrapKey('raw', rsaWrapped, rsaWrap.privateKey, {name:'RSA-OAEP',label:new Uint8Array([9])}, {name:'AES-GCM'}, false, ['encrypt']); throw Error('Accepted wrong OAEP unwrap label'); } catch (error) { if (error.message === 'Accepted wrong OAEP unwrap label' || error.name !== 'OperationError') throw error; } const clone = structuredClone(derived); if (clone.extractable || clone.algorithm.length !== 128 || clone.usages.join(',') !== 'encrypt,decrypt') throw Error('CryptoKey clone policy'); try { await subtle.exportKey('raw', clone); throw Error('Clone became extractable'); } catch(e) { if (e.name !== 'InvalidAccessError') throw e; } const clonedPassword = structuredClone(pb); if (clonedPassword.extractable || clonedPassword.type !== 'secret' || clonedPassword.algorithm.name !== 'PBKDF2' || clonedPassword.usages.join(',') !== 'deriveBits,deriveKey') throw Error('Nonextractable CryptoKey metadata'); const cloneBits = await subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt:new TextEncoder().encode('salt'),iterations:1}, clonedPassword, 256); if (hex(cloneBits) !== hex(pbBits)) throw Error('CryptoKey native policy/material lost'); try { await subtle.exportKey('raw', clonedPassword); throw Error('Nonextractable clone became extractable'); } catch(e) { if (e.name !== 'InvalidAccessError') throw e; } console.log('kdf-kw'); "#, &[], ); assert_eq!(out.stdout, b"kdf-kw\n"); } #[test] fn web_crypto_digest_vectors_and_snapshots() { let out = common::run( r#" const hex = async promise => [...new Uint8Array(await promise)].map(x => x.toString(16).padStart(2, '0')).join(''); const input = new TextEncoder().encode('abc'); const vectors = [ ['SHA-1', 'a9993e364706816aba3e25717850c26c9cd0d89d'], ['SHA-256', 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'], ['SHA-384', 'cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7'], ['SHA-512', 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'], ]; for (const [name, expected] of vectors) { if (await hex(crypto.subtle.digest(name.toLowerCase(), input)) !== expected) throw Error(name); } const backing = new Uint8Array([97, 98, 99]); const pending = crypto.subtle.digest('SHA-256', backing); backing.fill(0); if (await hex(pending) !== vectors[1][1]) throw Error('Input snapshot'); const offset = new Uint8Array([9, 97, 98, 99, 9]); if (await hex(crypto.subtle.digest({name:'SHA-256'}, offset.subarray(1, 4))) !== vectors[1][1]) throw Error('View bounds'); try { await crypto.subtle.digest('MD5', input); throw Error('Accepted digest'); } catch (error) { if (error.name !== 'NotSupportedError') throw error; } for (const bad of [null, {}, [], 'abc', 1]) { try { await crypto.subtle.digest('SHA-256', bad); throw Error('Accepted invalid input'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } try { await crypto.subtle.digest.call({}, 'SHA-256', input); throw Error('Brand'); } catch (error) { if (!(error instanceof TypeError)) throw error; } console.log('crypto-digest'); "#, &[], ); assert_eq!(out.stdout, b"crypto-digest\n"); } #[test] fn web_crypto_interfaces_and_public_key_policies_survive_cloning() { let out = common::run( r#" for(const ctor of [Crypto,SubtleCrypto,CryptoKey]){ try{new ctor();throw Error('Constructed illegal interface');}catch(e){if(!(e instanceof TypeError))throw e;} } const rejected=crypto.subtle.digest({},new Uint8Array()); if(!(rejected instanceof Promise))throw Error('Digest did not return promise'); try{await rejected;throw Error('Missing algorithm accepted');}catch(e){if(!(e instanceof TypeError))throw e;} try{await crypto.subtle.digest('ſHA-256',new Uint8Array());throw Error('Unicode case-folded algorithm');}catch(e){if(e.name!=='NotSupportedError')throw e;} const bytes=new Uint8Array([97,98,99]);bytes.buffer.transfer(); if((await crypto.subtle.digest('SHA-256',bytes)).byteLength!==32||crypto.getRandomValues(bytes)!==bytes)throw Error('Empty detached views'); const pair=await crypto.subtle.generateKey('Ed25519',true,['sign','verify']); const raw=await crypto.subtle.exportKey('raw',pair.publicKey); const publicKey=await crypto.subtle.importKey('raw',raw,'Ed25519',false,['verify']); const clone=structuredClone(publicKey); const signature=await crypto.subtle.sign('Ed25519',pair.privateKey,new Uint8Array([1])); for(const key of [publicKey,clone]){ if(key.extractable||key.type!=='public'||!await crypto.subtle.verify('Ed25519',key,signature,new Uint8Array([1])))throw Error('Public clone policy'); try{await crypto.subtle.exportKey('raw',key);throw Error('Exported nonextractable public key');}catch(e){if(e.name!=='InvalidAccessError')throw e;} } const aes=await crypto.subtle.generateKey({name:'AES-GCM',length:128},true,['encrypt','decrypt']); const longIv=new Uint8Array(65);for(let i=0;i<65;i++)longIv[i]=i; const longCipher=await crypto.subtle.encrypt({name:'AES-GCM',iv:longIv},aes,new Uint8Array([1,2,3])); if([...new Uint8Array(await crypto.subtle.decrypt({name:'AES-GCM',iv:longIv},aes,longCipher))].join(',')!=='1,2,3')throw Error('Long IV roundtrip'); console.log('crypto policies'); "#, &[], ); assert_eq!(out.stdout, b"crypto policies\n"); } #[test] fn crypto_counter_wraps_and_field_element_lengths_match_the_standard() { let out = common::run( r#" const s=crypto.subtle, hex=b=>[...new Uint8Array(b)].map(x=>x.toString(16).padStart(2,'0')).join(''); const key=await s.importKey('raw',new Uint8Array(16).fill(1),'AES-CTR',false,['encrypt']); const counter=new Uint8Array(16).fill(2);counter[15]=254; if(hex(await s.encrypt({name:'AES-CTR',counter,length:8},key,new Uint8Array(48)))!=='f2a6365950d796f031b1526b9ef9468a0a32c8129e404de4f831f721158b6691e702bcca70e79b86f6b15d5de68c37e0')throw Error('Counter carried into nonce'); for(const bits of [1,8]){ try{await s.importKey('raw',new Uint8Array(2),{name:'HMAC',hash:'SHA-256',length:bits},false,['sign']);throw Error('HMAC dropped whole bytes');}catch(e){if(e.name!=='DataError')throw e;} } const a=await s.generateKey({name:'ECDH',namedCurve:'P-521'},false,['deriveBits']); const b=await s.generateKey({name:'ECDH',namedCurve:'P-521'},false,['deriveBits']); const all=await s.deriveBits({name:'ECDH',public:b.publicKey},a.privateKey); const explicit=await s.deriveBits({name:'ECDH',public:b.publicKey},a.privateKey,528); if(all.byteLength!==66||hex(all)!==hex(explicit)||(await s.deriveBits({name:'ECDH',public:b.publicKey},a.privateKey,0)).byteLength!==0)throw Error('Field element bit length'); const kdf=await s.importKey('raw',new Uint8Array(),'PBKDF2',false,['deriveBits']); if((await s.deriveBits({name:'PBKDF2',hash:'SHA-256',salt:new Uint8Array(),iterations:1},kdf,0)).byteLength!==0)throw Error('Empty derived bits'); console.log('crypto edge vectors'); "#, &[], ); assert_eq!(out.stdout, b"crypto edge vectors\n"); } #[test] fn crypto_export_errors_and_raw_hmac_unwrap_keep_key_policy() { let out = common::run( r#" const s=crypto.subtle; const pair=await s.generateKey({name:'ECDSA',namedCurve:'P-256'},true,['sign','verify']); if(Object.getOwnPropertyNames(pair.privateKey).length!==0)throw Error('Unexpected key properties'); for(const [format,key] of [['raw',pair.privateKey],['spki',pair.privateKey],['pkcs8',pair.publicKey]]){ try{await s.exportKey(format,key);throw Error('Wrong key type exported');}catch(e){if(e.name!=='InvalidAccessError')throw e;} } try{await s.exportKey('RAW',pair.publicKey);throw Error('Invalid format enum accepted');}catch(e){if(!(e instanceof TypeError))throw e;} const kek=await s.generateKey({name:'AES-GCM',length:128},false,['wrapKey','unwrapKey']); const key=await s.importKey('raw',new Uint8Array([255,255]),{name:'HMAC',hash:'SHA-256'},true,['sign']); const algorithm={name:'AES-GCM',iv:new Uint8Array(12)}; const wrapped=await s.wrapKey('raw',key,kek,algorithm); const unwrapped=await s.unwrapKey('raw',wrapped,kek,algorithm,{name:'HMAC',hash:'SHA-256',length:9},true,['sign']); if(unwrapped.algorithm.length!==9||[...new Uint8Array(await s.exportKey('raw',unwrapped))].join(',')!=='255,128')throw Error('HMAC unwrap ignored length'); Object.prototype.salt=new Uint8Array([255]); Object.prototype.info=new Uint8Array([255]); Object.prototype.counter=new Uint8Array([255]); try{ const hmac=await s.sign('HMAC',key,new Uint8Array([0])); if(hmac.byteLength!==32)throw Error('Prototype altered internal parameters'); }finally{delete Object.prototype.salt;delete Object.prototype.info;delete Object.prototype.counter;} console.log('crypto review fixes'); "#, &[], ); assert_eq!(out.stdout, b"crypto review fixes\n"); } #[test] fn crypto_binary_results_are_array_buffers_and_inputs_are_not_shared() { let out = common::run( r#" const s=crypto.subtle, data=new Uint8Array([1]); const aes=await s.generateKey({name:'AES-GCM',length:128},true,['encrypt','decrypt','wrapKey','unwrapKey']); const mac=await s.generateKey({name:'HMAC',hash:'SHA-256'},true,['sign']); const kdf=await s.importKey('raw',data,'HKDF',false,['deriveBits']); const algorithm={name:'AES-GCM',iv:new Uint8Array(12)}; const cipher=await s.encrypt(algorithm,aes,data); const outputs=[ await s.digest('SHA-256',data),cipher,await s.decrypt(algorithm,aes,cipher), await s.sign('HMAC',mac,data),await s.exportKey('raw',aes), await s.wrapKey('raw',aes,aes,algorithm), await s.deriveBits({name:'HKDF',hash:'SHA-256',salt:data,info:data},kdf,8) ]; for(const output of outputs)if(!(output instanceof ArrayBuffer)||ArrayBuffer.isView(output))throw Error('Wrong binary output type'); for(const source of [new SharedArrayBuffer(1),new Uint8Array(new SharedArrayBuffer(1)),new DataView(new SharedArrayBuffer(1))]){ try{await s.digest('SHA-256',source);throw Error('Shared BufferSource accepted');}catch(e){if(!(e instanceof TypeError))throw e;} } const ignored={get hash(){throw Error('Read ignored hash');},get saltLength(){throw Error('Read ignored saltLength');}}; ignored.name='HMAC'; if((await s.sign(ignored,mac,data)).byteLength!==32)throw Error('Ignored fields'); console.log('crypto binary types'); "#, &[], ); assert_eq!(out.stdout, b"crypto binary types\n"); } #[test] fn rsa_pss_salt_lengths_use_enforce_range_dictionary_conversion() { let out = common::run( r#" const subtle=crypto.subtle; const pair=await subtle.generateKey({name:'RSA-PSS',modulusLength:1024,publicExponent:new Uint8Array([1,0,1]),hash:'SHA-256'},false,['sign','verify']); const message=new Uint8Array([1,2,3]); const signature=await subtle.sign({name:'RSA-PSS',saltLength:'32'},pair.privateKey,message); if(!await subtle.verify({name:'RSA-PSS',saltLength:32.9},pair.publicKey,signature,message))throw Error('Salt conversion'); for(const saltLength of [-1,2**40,NaN,Infinity,-Infinity,undefined,1n,Symbol()]){ for(const invoke of [()=>subtle.sign({name:'RSA-PSS',saltLength},pair.privateKey,message),()=>subtle.verify({name:'RSA-PSS',saltLength},pair.publicKey,signature,message)]){ try{await invoke();throw Error('Invalid salt accepted');}catch(e){if(!(e instanceof TypeError))throw e;} } } let reads=0; const zero=await subtle.sign({name:'RSA-PSS',get saltLength(){reads++;return -0.5;}},pair.privateKey,message); if(reads!==1||!await subtle.verify({name:'RSA-PSS',saltLength:0},pair.publicKey,zero,message))throw Error('Salt getter or truncation'); console.log('salt conversion'); "#, &[], ); assert_eq!(out.stdout, b"salt conversion\n"); }