Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support generating ECDSA keys in a trusted execution environment #2281

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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