diff --git a/doc/api/SubtleCrypto.json b/doc/api/SubtleCrypto.json index 24a87276..0eb96440 100644 --- a/doc/api/SubtleCrypto.json +++ b/doc/api/SubtleCrypto.json @@ -267,7 +267,8 @@ "union": [ "'spki'", "'pkcs8'", - "'raw'" + "'raw'", + "'teeKeyHandle'" ] } }, @@ -349,6 +350,16 @@ { "name": "keyUsages", "type": "string[]" + }, + { + "name": "options", + "optional": true, + "type": { + "map": { + "inTee": { "type": "boolean", "optional": true }, + "usageRequiresAuth": { "type": "boolean", "optional": true } + } + } } ], "returns": { @@ -467,7 +478,8 @@ "type": { "union": [ "'raw'", - "'spki'" + "'spki'", + "'teeKeyHandle'" ] } }, diff --git a/snippets/crypto-sign.ts b/snippets/crypto-sign.ts index 788f41e4..0f26ec10 100644 --- a/snippets/crypto-sign.ts +++ b/snippets/crypto-sign.ts @@ -6,28 +6,58 @@ tabris.onLog(({message}) => stack.append(TextView({text: message}))); (async function() { - // Generate a key pair for signing and verifying - const keyPair = await crypto.subtle.generateKey( - {name: 'ECDSA', namedCurve: 'P-256'}, - true, - ['sign', 'verify'] - ); - - // Sign a message - const message = await new Blob(['Message']).arrayBuffer(); - const signature = await crypto.subtle.sign( - {name: 'ECDSAinDERFormat', hash: 'SHA-256'}, - keyPair.privateKey, - message - ); - console.log('Signature:', new Uint8Array(signature).join(', ')); - - // Verify the signature - const isValid = await crypto.subtle.verify( - {name: 'ECDSAinDERFormat', hash: 'SHA-256'}, - keyPair.publicKey, - signature, message - ); - console.log('Signature valid:', isValid); + { + console.log('ECDSA signing/verification with generated keys:'); + const generationAlgorithm = {name: 'ECDSA' as const, namedCurve: 'P-256' as const}; + const signingAlgorithm = {name: 'ECDSAinDERFormat' as const, hash: 'SHA-256' as const}; + + // Generate a key pair for signing and verifying + const keyPair = await crypto.subtle.generateKey(generationAlgorithm, true, ['sign', 'verify']); + + // Export the public key and import it back + const publicKeySpki = await crypto.subtle.exportKey('spki', keyPair.publicKey); + const publicKey = await crypto.subtle.importKey('spki', publicKeySpki, generationAlgorithm, true, ['verify']); + + // Sign a message + const message = await new Blob(['Message']).arrayBuffer(); + const signature = await crypto.subtle.sign(signingAlgorithm, keyPair.privateKey, message); + console.log('Signature:', new Uint8Array(signature).join(', ')); + + // Verify the signature + const isValid = await crypto.subtle.verify(signingAlgorithm, publicKey, signature, message); + console.log('Signature valid:', isValid); + } + + { + console.log('ECDSA signing/verification with keys generated in a trusted execution environment (TEE):'); + const generationAlgorithm = {name: 'ECDSA' as const, namedCurve: 'P-256' as const}; + const signingAlgorithm = {name: 'ECDSAinDERFormat' as const, hash: 'SHA-256' as const}; + + // Generate a key pair for signing and verifying + const keyPair = await crypto.subtle.generateKey( + generationAlgorithm, + true, + ['sign', 'verify'], + {inTee: true, usageRequiresAuth: true} + ); + + // Export the private key and import it back + const privateKeyHandle = await crypto.subtle.exportKey('teeKeyHandle', keyPair.privateKey); + const algorithm = {name: 'ECDSA' as const, namedCurve: 'P-256' as const}; + const privateKey = await crypto.subtle.importKey('teeKeyHandle', privateKeyHandle, algorithm, true, ['sign']); + + // Export the public key and import it back + const publicKeySpki = await crypto.subtle.exportKey('spki', keyPair.publicKey); + const publicKey = await crypto.subtle.importKey('spki', publicKeySpki, algorithm, true, ['verify']); + + // Sign a message + const message = await new Blob(['Message']).arrayBuffer(); + const signature = await crypto.subtle.sign(signingAlgorithm, privateKey, message); + console.log('Signature:', new Uint8Array(signature).join(', ')); + + // Verify the signature + const isValid = await crypto.subtle.verify(signingAlgorithm, publicKey, signature, message); + console.log('Signature valid:', isValid); + } }()); diff --git a/src/tabris/Crypto.ts b/src/tabris/Crypto.ts index 0726b345..60f51fa3 100644 --- a/src/tabris/Crypto.ts +++ b/src/tabris/Crypto.ts @@ -6,6 +6,7 @@ import CryptoKey, { AlgorithmECDSA, AlgorithmHKDF, AlgorithmInternal, + GenerateKeyOptions, _CryptoKey } from './CryptoKey'; import {allowOnlyKeys, allowOnlyValues, getBuffer, getCid, getNativeObject} from './util'; @@ -81,7 +82,7 @@ class SubtleCrypto { if (arguments.length !== 5) { throw new TypeError(`Expected 5 arguments, got ${arguments.length}`); } - allowOnlyValues(format, ['spki', 'pkcs8', 'raw'], 'format'); + allowOnlyValues(format, ['spki', 'pkcs8', 'raw', 'teeKeyHandle'], 'format'); checkType(getBuffer(keyData), ArrayBuffer, {name: 'keyData'}); if (typeof algorithm === 'string') { allowOnlyValues(algorithm, ['AES-GCM', 'HKDF'], 'algorithm'); @@ -205,13 +206,13 @@ class SubtleCrypto { } async exportKey( - format: 'raw' | 'spki', + format: 'raw' | 'spki' | 'teeKeyHandle', key: CryptoKey ): Promise { if (arguments.length !== 2) { throw new TypeError(`Expected 2 arguments, got ${arguments.length}`); } - allowOnlyValues(format, ['raw', 'spki'], 'format'); + allowOnlyValues(format, ['raw', 'spki', 'teeKeyHandle'], 'format'); checkType(key, CryptoKey, {name: 'key'}); return new Promise((onSuccess, onReject) => this._nativeObject.subtleExportKey(format, key, onSuccess, onReject) @@ -221,18 +222,36 @@ class SubtleCrypto { async generateKey( algorithm: AlgorithmECDH | AlgorithmECDSA, extractable: boolean, - keyUsages: string[] + keyUsages: string[], + options?: GenerateKeyOptions ): Promise<{privateKey: CryptoKey, publicKey: CryptoKey}> { - if (arguments.length !== 3) { - throw new TypeError(`Expected 3 arguments, got ${arguments.length}`); + if (arguments.length < 3) { + throw new TypeError(`Expected at least 3 arguments, got ${arguments.length}`); } allowOnlyKeys(algorithm, ['name', 'namedCurve']); allowOnlyValues(algorithm.name, ['ECDH', 'ECDSA'], 'algorithm.name'); allowOnlyValues(algorithm.namedCurve, ['P-256'], 'algorithm.namedCurve'); checkType(extractable, Boolean, {name: 'extractable'}); checkType(keyUsages, Array, {name: 'keyUsages'}); + if (options != null) { + allowOnlyKeys(options, ['inTee', 'usageRequiresAuth']); + if('inTee' in options) { + checkType(options.inTee, Boolean, {name: 'options.inTee'}); + } + if ('usageRequiresAuth' in options) { + checkType(options.usageRequiresAuth, Boolean, {name: 'options.usageRequiresAuth'}); + } + if (options.inTee && algorithm.name !== 'ECDSA') { + throw new TypeError('options.inTee is only supported for ECDSA keys'); + } + if (options.usageRequiresAuth && algorithm.name !== 'ECDSA') { + throw new TypeError('options.usageRequiresAuth is only supported for ECDSA keys'); + } + } + const inTee = options?.inTee; + const usageRequiresAuth = options?.usageRequiresAuth; const nativeObject = new _CryptoKey(); - await nativeObject.generate(algorithm, extractable, keyUsages); + await nativeObject.generate(algorithm, extractable, keyUsages, inTee, usageRequiresAuth); const nativePrivate = new _CryptoKey(nativeObject, 'private'); const nativePublic = new _CryptoKey(nativeObject, 'public'); return { diff --git a/src/tabris/CryptoKey.ts b/src/tabris/CryptoKey.ts index ecfb730b..ccfb887d 100644 --- a/src/tabris/CryptoKey.ts +++ b/src/tabris/CryptoKey.ts @@ -24,6 +24,8 @@ export type AlgorithmECDSA = { namedCurve: 'P-256' }; +export type GenerateKeyOptions = { inTee?: boolean, usageRequiresAuth?: boolean }; + export default class CryptoKey { constructor(nativeObject: _CryptoKey, data: CryptoKey) { @@ -114,13 +116,17 @@ export class _CryptoKey extends NativeObject { async generate( algorithm: AlgorithmECDH | AlgorithmECDSA, extractable: boolean, - keyUsages: string[] + keyUsages: string[], + inTee?: boolean, + usageRequiresAuth?: boolean ): Promise { return new Promise((onSuccess, onError) => this._nativeCall('generate', { algorithm, extractable, keyUsages, + inTee, + usageRequiresAuth, onSuccess, onError: wrapErrorCb(onError) }) diff --git a/test/tabris/Crypto.test.ts b/test/tabris/Crypto.test.ts index 492befb2..fd8c1251 100644 --- a/test/tabris/Crypto.test.ts +++ b/test/tabris/Crypto.test.ts @@ -633,7 +633,7 @@ describe('Crypto', function() { it('checks format values', async function() { params[0] = 'foo'; await expect(importKey()) - .rejectedWith(TypeError, 'format must be "spki", "pkcs8" or "raw", got "foo"'); + .rejectedWith(TypeError, 'format must be "spki", "pkcs8", "raw" or "teeKeyHandle", got "foo"'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); @@ -750,7 +750,7 @@ describe('Crypto', function() { // @ts-ignore params[0] = 'foo'; await expect(exportKey()) - .rejectedWith(TypeError, 'format must be "raw" or "spki", got "foo"'); + .rejectedWith(TypeError, 'format must be "raw", "spki" or "teeKeyHandle", got "foo"'); expect(client.calls({op: 'call', method: 'subtleExportKey'}).length).to.equal(0); }); @@ -1034,19 +1034,25 @@ describe('Crypto', function() { beforeEach(function() { client.resetCalls(); params = [ - { - name: 'ECDH', - namedCurve: 'P-256' - }, + {name: 'ECDSA', namedCurve: 'P-256'}, true, - ['foo', 'bar'] + ['foo', 'bar'], + {inTee: true, usageRequiresAuth: true} ]; }); it('CREATEs CryptKey and CALLs generate', async function() { await generateKey(param => param.onSuccess()); const id = client.calls({op: 'create', type: 'tabris.CryptoKey'})[0].id; - expect(client.calls({op: 'call', id, method: 'generate'}).length).to.equal(1); + const calls = client.calls({op: 'call', id, method: 'generate'}); + expect(calls.length).to.equal(1); + expect(calls[0].parameters).to.deep.include({ + algorithm: {name: 'ECDSA', namedCurve: 'P-256'}, + extractable: true, + keyUsages: ['foo', 'bar'], + inTee: true, + usageRequiresAuth: true + }); }); it('CREATEs public and private CryptKey', async function() { @@ -1067,9 +1073,10 @@ describe('Crypto', function() { }); it('checks parameter length', async function() { - params.pop(); + params.pop(); // removes optional parameter `options` + params.pop(); // removes required parameter `usages` await expect(generateKey()) - .rejectedWith(TypeError, 'Expected 3 arguments, got 2'); + .rejectedWith(TypeError, 'Expected at least 3 arguments, got 2'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); @@ -1103,6 +1110,36 @@ describe('Crypto', function() { expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); + it('checks options.inTee type', async function() { + params[3] = {inTee: null, usageRequiresAuth: true}; + await expect(generateKey()) + .rejectedWith(TypeError, 'Expected options.inTee to be a boolean, got null'); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); + }); + + it('checks options.usageRequiresAuth type', async function() { + params[3] = {inTee: true, usageRequiresAuth: null}; + await expect(generateKey()) + .rejectedWith(TypeError, 'Expected options.usageRequiresAuth to be a boolean, got null'); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); + }); + + it('rejects options.inTee when algorithm name is not ECDSA', async function() { + params[0] = {name: 'ECDH', namedCurve: 'P-256'}; + params[3] = {inTee: true}; + await expect(generateKey()) + .rejectedWith(TypeError, 'options.inTee is only supported for ECDSA keys'); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); + }); + + it('rejects options.usageRequiresAuth when algorithm name is not ECDSA', async function() { + params[0] = {name: 'ECDH', namedCurve: 'P-256'}; + params[3] = {usageRequiresAuth: true}; + await expect(generateKey()) + .rejectedWith(TypeError, 'options.usageRequiresAuth is only supported for ECDSA keys'); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); + }); + }); describe('subtle.sign()', function() {