From d43e7d3143119f034d2fc85e58f2ed98e4175a7e 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 | 59 ++++++++++++++++++++++++++--------- src/tabris/Crypto.ts | 36 ++++++++++++++++----- src/tabris/CryptoKey.ts | 8 ++++- test/tabris/Crypto.test.ts | 64 ++++++++++++++++++++++++++++++++------ 5 files changed, 149 insertions(+), 34 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..3d2f7c56 100644 --- a/snippets/crypto-sign.ts +++ b/snippets/crypto-sign.ts @@ -5,29 +5,60 @@ const stack = Stack({stretch: true, spacing: 8, padding: 16, alignment: 'stretch tabris.onLog(({message}) => stack.append(TextView({text: message}))); (async function() { + await signAndVerify(); + await signAndVerifyWithKeysInTeeRequiringAuth(); +}()).catch(console.error); + +async function signAndVerify() { + 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); +} + +async function signAndVerifyWithKeysInTeeRequiringAuth() { + 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( - {name: 'ECDSA', namedCurve: 'P-256'}, + generationAlgorithm, true, - ['sign', 'verify'] + ['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( - {name: 'ECDSAinDERFormat', hash: 'SHA-256'}, - keyPair.privateKey, - message - ); + 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( - {name: 'ECDSAinDERFormat', hash: 'SHA-256'}, - keyPair.publicKey, - signature, message - ); + 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..c6f505cd 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,39 @@ 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'); + } + if (options.usageRequiresAuth && !options.inTee) { + throw new TypeError('options.usageRequiresAuth is only supported for keys in TEE'); + } + } + 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..d73fad55 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,43 @@ 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); + }); + + it('rejects options.usageRequiresAuth when options.inTee is not set', async function() { + params[3] = {usageRequiresAuth: true}; + await expect(generateKey()) + .rejectedWith(TypeError, 'options.usageRequiresAuth is only supported for keys in TEE'); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); + }); + }); describe('subtle.sign()', function() {