Skip to content

Commit

Permalink
EC keys: consolidate API for handling keys in TEE
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cpetrov committed Feb 5, 2024
1 parent 8d66003 commit 040079c
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 57 deletions.
11 changes: 4 additions & 7 deletions doc/api/SubtleCrypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,7 @@
"union": [
"'spki'",
"'pkcs8'",
"'raw'",
"'teeKeyHandle'"
"'raw'"
]
}
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -376,7 +375,6 @@
"optional": true,
"type": {
"map": {
"inTee": { "type": "boolean", "optional": true },
"usageRequiresAuth": { "type": "boolean", "optional": true }
}
}
Expand Down Expand Up @@ -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'"
]
}
},
Expand Down
9 changes: 6 additions & 3 deletions snippets/crypto-derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -103,15 +103,18 @@ 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};

// Generate Alice's ECDH key pair
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');
Expand Down
20 changes: 10 additions & 10 deletions snippets/crypto-sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,37 @@ 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};

// 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 privateKeyHandle = 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', privateKeyHandle, 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(', '));

Expand Down
18 changes: 7 additions & 11 deletions src/tabris/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -220,13 +220,13 @@ class SubtleCrypto {
}

async exportKey(
format: 'raw' | 'spki' | 'teeKeyHandle',
format: 'raw' | 'spki',
key: CryptoKey
): Promise<ArrayBuffer> {
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)
Expand All @@ -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 {
Expand Down
4 changes: 1 addition & 3 deletions src/tabris/CryptoKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -123,15 +123,13 @@ export class _CryptoKey extends NativeObject {
algorithm: AlgorithmECDH | AlgorithmECDSA,
extractable: boolean,
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
38 changes: 15 additions & 23 deletions test/tabris/Crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

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

Expand Down Expand Up @@ -1065,7 +1065,7 @@ describe('Crypto', function() {
{name: 'ECDSA', namedCurve: 'P-256'},
true,
['foo', 'bar'],
{inTee: true, usageRequiresAuth: true}
{usageRequiresAuth: false}
];
});

Expand All @@ -1078,8 +1078,7 @@ describe('Crypto', function() {
algorithm: {name: 'ECDSA', namedCurve: 'P-256'},
extractable: true,
keyUsages: ['foo', 'bar'],
inTee: true,
usageRequiresAuth: true
usageRequiresAuth: false
});
});

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

});

Expand Down

0 comments on commit 040079c

Please sign in to comment.