From 587a62cc6190d40749305fd7fa2a6a6726d61e64 Mon Sep 17 00:00:00 2001 From: Christian Petrov Date: Mon, 5 Feb 2024 18:39:32 +0000 Subject: [PATCH] EC keys: consolidate API for handling keys in TEE The `inTee` option and the `teeKeyHandle` format provided a way to create keys in the TEE and export them as opaque handles. While this API made it transparent when a key is really stored in the TEE, it was not convenient in practice, as it required the application to conduct specific checks for hardware support on Android in order to decide whether to store the key in TEE or not. This commit removes the `inTee` option. ECDSA and ECDH keys with `extractable` set to `false` are now generated in the TEE by the platform when the device supports it. When the device does not support it, the key is generated in software. In practice, this means that non-extractable EC* keys are always generated in TEE on iOS, and on Android only when the device supports it. The `"teeKeyHandle"` format has also been removed. The `exportKey` method called for EC* keys with `extractable` set to `false` and the format `"raw"` now returns an opaque handle to the key in the TEE instead of throwing. --- doc/api/SubtleCrypto.json | 11 ++++------- snippets/crypto-derive.ts | 9 ++++++--- snippets/crypto-sign.ts | 20 ++++++++++---------- src/tabris/Crypto.ts | 18 +++++++----------- src/tabris/CryptoKey.ts | 4 +--- test/tabris/Crypto.test.ts | 38 +++++++++++++++----------------------- 6 files changed, 43 insertions(+), 57 deletions(-) diff --git a/doc/api/SubtleCrypto.json b/doc/api/SubtleCrypto.json index e56a4a56..85c24132 100644 --- a/doc/api/SubtleCrypto.json +++ b/doc/api/SubtleCrypto.json @@ -287,8 +287,7 @@ "union": [ "'spki'", "'pkcs8'", - "'raw'", - "'teeKeyHandle'" + "'raw'" ] } }, @@ -346,7 +345,7 @@ } }, "generateKey": { - "description": "Generates new keys. Currently only supports the Elliptic Curve Diffie-Hellman (ECDH) algorithm to generate key pairs.", + "description": "Generates new keys. Currently only supports the Elliptic Curve Diffie-Hellman (ECDH) and Elliptic Curve Digital Signature Algorithm (ECDSA) algorithms to generate key pairs. When `extractable` is set to `true`, the raw key material can be exported using `exportKey`. When `extractable` is set to `false`, for ECDSA and ECDH keys `exportKey` returns an opaque handle to the key in the device's trusted execution environment, and throws for other key formats.", "parameters": [ { "name": "algorithm", @@ -376,7 +375,6 @@ "optional": true, "type": { "map": { - "inTee": { "type": "boolean", "optional": true }, "usageRequiresAuth": { "type": "boolean", "optional": true } } } @@ -491,15 +489,14 @@ } }, "exportKey": { - "description": "Converts a CryptoKey instances into a portable format. To export a key, the key must have extractable set to true. Supports the spki format or raw bytes.", + "description": "Converts `CryptoKey` instances into a portable format. If the key's `extractable` is set to `true`, returns the raw key material in SPKI format or as raw bytes. If the key's `extractable` is set to `false`, for ECDSA and ECDH keys returns an opaque handle to the key in the device's trusted execution environment, and throws for other key formats.", "parameters": [ { "name": "format", "type": { "union": [ "'raw'", - "'spki'", - "'teeKeyHandle'" + "'spki'" ] } }, diff --git a/snippets/crypto-derive.ts b/snippets/crypto-derive.ts index 4710a673..4c2e4841 100644 --- a/snippets/crypto-derive.ts +++ b/snippets/crypto-derive.ts @@ -8,7 +8,7 @@ tabris.onLog(({message}) => stack.append(TextView({text: message}))); (async () => { await importAndDerive(); await generateDeriveEncryptAndDecrypt(); - await generateDeriveEncryptAndDecrypt({inTee: true, usageRequiresAuth: true}); + await generateDeriveEncryptAndDecrypt({extractable: false, usageRequiresAuth: true}); })().catch(console.error); async function importAndDerive() { @@ -103,7 +103,10 @@ async function importAndDerive() { } } -async function generateDeriveEncryptAndDecrypt({inTee, usageRequiresAuth} = {inTee: false, usageRequiresAuth: false}) { +async function generateDeriveEncryptAndDecrypt({extractable, usageRequiresAuth} = { + extractable: true, + usageRequiresAuth: false +}) { const ecdhP256 = {name: 'ECDH' as const, namedCurve: 'P-256' as const}; const aesGcm = {name: 'AES-GCM' as const}; @@ -111,7 +114,7 @@ async function generateDeriveEncryptAndDecrypt({inTee, usageRequiresAuth} = {inT const alicesKeyPair = await crypto.subtle.generateKey(ecdhP256, true, ['deriveBits']); // Generate Bob's ECDH key pair - const bobsKeyPair = await crypto.subtle.generateKey(ecdhP256, true, ['deriveBits'], {inTee, usageRequiresAuth}); + const bobsKeyPair = await crypto.subtle.generateKey(ecdhP256, extractable, ['deriveBits'], {usageRequiresAuth}); // Derive Alice's AES key const alicesAesKey = await deriveAesKey(bobsKeyPair.publicKey, alicesKeyPair.privateKey, 'encrypt'); diff --git a/snippets/crypto-sign.ts b/snippets/crypto-sign.ts index 97889c1c..a3327300 100644 --- a/snippets/crypto-sign.ts +++ b/snippets/crypto-sign.ts @@ -6,10 +6,10 @@ tabris.onLog(({message}) => stack.append(TextView({text: message}))); (async function() { await signAndVerify(); - await signAndVerify({inTee: true, usageRequiresAuth: true}); + await signAndVerify({extractable: false, usageRequiresAuth: true}); }()).catch(console.error); -async function signAndVerify({inTee, usageRequiresAuth} = {inTee: false, usageRequiresAuth: false}) { +async function signAndVerify({extractable, usageRequiresAuth} = {extractable: true, usageRequiresAuth: false}) { 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}; @@ -17,26 +17,26 @@ async function signAndVerify({inTee, usageRequiresAuth} = {inTee: false, usageRe // Generate a key pair for signing and verifying const keyPair = await crypto.subtle.generateKey( generationAlgorithm, - true, + extractable, ['sign', 'verify'], - {inTee, usageRequiresAuth} + {usageRequiresAuth} ); let privateKeyImportedFromTee: CryptoKey; - if (inTee) { - // Export the private key and import it back - const privateKeyHandle = await crypto.subtle.exportKey('teeKeyHandle', keyPair.privateKey); + if (!extractable) { + // Export a handle of the private key stored in the Trusted Execution Environment and import it back + const exportedKey = await crypto.subtle.exportKey('raw', keyPair.privateKey); const alg = {name: 'ECDSA' as const, namedCurve: 'P-256' as const}; - privateKeyImportedFromTee = await crypto.subtle.importKey('teeKeyHandle', privateKeyHandle, alg, true, ['sign']); + privateKeyImportedFromTee = await crypto.subtle.importKey('raw', exportedKey, alg, extractable, ['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, generationAlgorithm, true, ['verify']); + const publicKey = await crypto.subtle.importKey('spki', publicKeySpki, generationAlgorithm, extractable, ['verify']); // Sign a message const message = await new Blob(['Message']).arrayBuffer(); - const privateKey = inTee ? privateKeyImportedFromTee : keyPair.privateKey; + const privateKey = !extractable ? privateKeyImportedFromTee : keyPair.privateKey; const signature = await crypto.subtle.sign(signingAlgorithm, privateKey, message); console.log('Signature:', new Uint8Array(signature).join(', ')); diff --git a/src/tabris/Crypto.ts b/src/tabris/Crypto.ts index 1a702b8d..5d334a7e 100644 --- a/src/tabris/Crypto.ts +++ b/src/tabris/Crypto.ts @@ -83,7 +83,7 @@ class SubtleCrypto { if (arguments.length !== 5) { throw new TypeError(`Expected 5 arguments, got ${arguments.length}`); } - allowOnlyValues(format, ['spki', 'pkcs8', 'raw', 'teeKeyHandle'], 'format'); + allowOnlyValues(format, ['spki', 'pkcs8', 'raw'], 'format'); checkType(getBuffer(keyData), ArrayBuffer, {name: 'keyData'}); if (typeof algorithm === 'string') { allowOnlyValues(algorithm, ['AES-GCM', 'HKDF'], 'algorithm'); @@ -220,13 +220,13 @@ class SubtleCrypto { } async exportKey( - format: 'raw' | 'spki' | 'teeKeyHandle', + format: 'raw' | 'spki', key: CryptoKey ): Promise { if (arguments.length !== 2) { throw new TypeError(`Expected 2 arguments, got ${arguments.length}`); } - allowOnlyValues(format, ['raw', 'spki', 'teeKeyHandle'], 'format'); + allowOnlyValues(format, ['raw', 'spki'], 'format'); checkType(key, CryptoKey, {name: 'key'}); return new Promise((onSuccess, onReject) => this._nativeObject.subtleExportKey(format, key, onSuccess, onReject) @@ -248,21 +248,17 @@ class SubtleCrypto { 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'}); - } + allowOnlyKeys(options, ['usageRequiresAuth']); if ('usageRequiresAuth' in options) { checkType(options.usageRequiresAuth, Boolean, {name: 'options.usageRequiresAuth'}); } - if (options.usageRequiresAuth && !options.inTee && (tabris as any).device.platform !== 'Android') { - throw new TypeError('options.usageRequiresAuth is only supported for keys not in TEE on Android'); + if (options.usageRequiresAuth && (extractable || !algorithm.name.startsWith('EC'))) { + throw new TypeError('options.usageRequiresAuth is only supported for non-extractable EC keys'); } } - const inTee = options?.inTee; const usageRequiresAuth = options?.usageRequiresAuth; const nativeObject = new _CryptoKey(); - await nativeObject.generate(algorithm, extractable, keyUsages, inTee, usageRequiresAuth); + await nativeObject.generate(algorithm, extractable, keyUsages, 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 6d577225..6f8df9ed 100644 --- a/src/tabris/CryptoKey.ts +++ b/src/tabris/CryptoKey.ts @@ -24,7 +24,7 @@ export type AlgorithmECDSA = { namedCurve: 'P-256' }; -export type GenerateKeyOptions = { inTee?: boolean, usageRequiresAuth?: boolean }; +export type GenerateKeyOptions = { usageRequiresAuth?: boolean }; export default class CryptoKey { @@ -123,7 +123,6 @@ export class _CryptoKey extends NativeObject { algorithm: AlgorithmECDH | AlgorithmECDSA, extractable: boolean, keyUsages: string[], - inTee?: boolean, usageRequiresAuth?: boolean ): Promise { return new Promise((onSuccess, onError) => @@ -131,7 +130,6 @@ export class _CryptoKey extends NativeObject { algorithm, extractable, keyUsages, - inTee, usageRequiresAuth, onSuccess, onError: wrapErrorCb(onError) diff --git a/test/tabris/Crypto.test.ts b/test/tabris/Crypto.test.ts index 853dc83d..5fce4d0d 100644 --- a/test/tabris/Crypto.test.ts +++ b/test/tabris/Crypto.test.ts @@ -661,7 +661,7 @@ describe('Crypto', function() { it('checks format values', async function() { params[0] = 'foo'; await expect(importKey()) - .rejectedWith(TypeError, 'format must be "spki", "pkcs8", "raw" or "teeKeyHandle", got "foo"'); + .rejectedWith(TypeError, 'format must be "spki", "pkcs8" or "raw", got "foo"'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); @@ -778,7 +778,7 @@ describe('Crypto', function() { // @ts-ignore params[0] = 'foo'; await expect(exportKey()) - .rejectedWith(TypeError, 'format must be "raw", "spki" or "teeKeyHandle", got "foo"'); + .rejectedWith(TypeError, 'format must be "raw" or "spki", got "foo"'); expect(client.calls({op: 'call', method: 'subtleExportKey'}).length).to.equal(0); }); @@ -1065,7 +1065,7 @@ describe('Crypto', function() { {name: 'ECDSA', namedCurve: 'P-256'}, true, ['foo', 'bar'], - {inTee: true, usageRequiresAuth: true} + {usageRequiresAuth: false} ]; }); @@ -1078,8 +1078,7 @@ describe('Crypto', function() { algorithm: {name: 'ECDSA', namedCurve: 'P-256'}, extractable: true, keyUsages: ['foo', 'bar'], - inTee: true, - usageRequiresAuth: true + usageRequiresAuth: false }); }); @@ -1138,35 +1137,28 @@ 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}; + params[3] = {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.usageRequiresAuth when options.inTee is not set and platform is not Android', async function() { - (tabris as any).device.platform = 'iOS'; + it('rejects options.usageRequiresAuth when key is extractable', async function() { + params[1] = true; params[3] = {usageRequiresAuth: true}; await expect(generateKey()) - .rejectedWith(TypeError, 'options.usageRequiresAuth is only supported for keys not in TEE on Android'); + .rejectedWith(TypeError, 'options.usageRequiresAuth is only supported for non-extractable EC keys'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); - it('does not reject options.usageRequiresAuth when options.inTee is not set and platform is Android', - async function() { - (tabris as any).device.platform = 'Android'; - params[3] = {usageRequiresAuth: true}; - await generateKey(param => param.onSuccess()); - expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.be.greaterThan(0); - }); + it('does not reject options.usageRequiresAuth for non-extractable EC keys', async function() { + (tabris as any).device.platform = 'Android'; + params[1] = false; + params[3] = {usageRequiresAuth: true}; + await generateKey(param => param.onSuccess()); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.be.greaterThan(0); + }); });