Skip to content

Commit

Permalink
Support generating ECDSA keys in a trusted execution environment
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cpetrov committed Dec 6, 2023
1 parent 7cedf01 commit 4d97efe
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 34 deletions.
16 changes: 14 additions & 2 deletions doc/api/SubtleCrypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@
"union": [
"'spki'",
"'pkcs8'",
"'raw'"
"'raw'",
"'teeKeyHandle'"
]
}
},
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -467,7 +478,8 @@
"type": {
"union": [
"'raw'",
"'spki'"
"'spki'",
"'teeKeyHandle'"
]
}
},
Expand Down
59 changes: 45 additions & 14 deletions snippets/crypto-sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}());
}
36 changes: 29 additions & 7 deletions src/tabris/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CryptoKey, {
AlgorithmECDSA,
AlgorithmHKDF,
AlgorithmInternal,
GenerateKeyOptions,
_CryptoKey
} from './CryptoKey';
import {allowOnlyKeys, allowOnlyValues, getBuffer, getCid, getNativeObject} from './util';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -205,13 +206,13 @@ class SubtleCrypto {
}

async exportKey(
format: 'raw' | 'spki',
format: 'raw' | 'spki' | 'teeKeyHandle',
key: CryptoKey
): Promise<ArrayBuffer> {
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)
Expand All @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion src/tabris/CryptoKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<void> {
return new Promise((onSuccess, onError) =>
this._nativeCall('generate', {
algorithm,
extractable,
keyUsages,
inTee,
usageRequiresAuth,
onSuccess,
onError: wrapErrorCb(onError)
})
Expand Down
64 changes: 54 additions & 10 deletions test/tabris/Crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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() {
Expand All @@ -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);
});

Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 4d97efe

Please sign in to comment.