From 6db15703e5179caa25865b6899b69483b7bc9965 Mon Sep 17 00:00:00 2001 From: Christian Petrov Date: Tue, 28 Nov 2023 15:53:25 +0000 Subject: [PATCH] Support generating ECDSA keys in a trusted execution environment This adds support for generating ECDSA keys in a trusted execution environment (TEE) that can be used for signing and verification. Such keys are generated in the TEE and never leave it. Because the keys are generated in the TEE, the keys themselves cannot be exported. Instead, a handle to the key is exported. The handle can be used to import the key and use it for signing and verification, but not to extract the key itself. A new `options` parameter is added to `crypto.subtle.generateKey()`. It has two optional properties: `inTee` and `usageRequiresAuth`. `inTee` is a boolean that indicates whether the key should be generated in a TEE. `usageRequiresAuth` is also a boolean that indicates whether the key can only be used when the user has authenticated. An example of how to generate and use a key in a TEE is added to the `crypto-sign` snippet. The snippet now contains two examples: one for generating and using keys in the normal way, and one for generating and using keys in a TEE. The `crypto-sign` snippets now also demonstrate exporting and importing keys. --- doc/api/SubtleCrypto.json | 16 +++++++- snippets/crypto-sign.ts | 76 ++++++++++++++++++++++++++------------ src/tabris/Crypto.ts | 33 +++++++++++++---- src/tabris/CryptoKey.ts | 8 +++- test/tabris/Crypto.test.ts | 57 +++++++++++++++++++++++----- 5 files changed, 147 insertions(+), 43 deletions(-) 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() {