Skip to content

Commit

Permalink
Support generating and importing ECDSA keys
Browse files Browse the repository at this point in the history
ECDSA and ECDH keys have the same internal representation on the native
client but are used for different purposes - ECDSA for signing and ECDH
for key exchange.

This commit adds support for ECDSA keys to the `SubtleCrypto` API. This
prepares the way for supporting `crypto.subtle.sign()` and
`crypto.subtle.verify()`.

It also removes `"ECDH"` from the allowed string values for the
`algorithm` parameter of `crypto.subtle.importKey`, because for ECDH and
ECDSA algorithms, always an object that includes `namedCurve` must be
provided [1].

[1]: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
  • Loading branch information
cpetrov committed Dec 6, 2023
1 parent 55c0c0d commit 295fac2
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 13 deletions.
2 changes: 1 addition & 1 deletion doc/api/CryptoKey.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"type": {
"map": {
"name": {
"type": "'ECDH'"
"type": { "union": ["'ECDH'", "'ECDSA'"] }
},
"namedCurve": {
"type": "'P-256'"
Expand Down
8 changes: 6 additions & 2 deletions doc/api/SubtleCrypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,9 @@
{
"map": {
"name": {
"type": "'ECDH'"
"type": {
"union": ["'ECDH'", "'ECDSA'"]
}
},
"namedCurve": {
"type": "'P-256'"
Expand Down Expand Up @@ -330,7 +332,9 @@
"type": {
"map": {
"name": {
"type": "'ECDH'"
"type": {
"union": ["'ECDH'", "'ECDSA'"]
}
},
"namedCurve": {
"type": "'P-256'"
Expand Down
19 changes: 13 additions & 6 deletions src/tabris/Crypto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import NativeObject from './NativeObject';
import {toValueString} from './Console';
import CryptoKey, {Algorithm, AlgorithmECDH, AlgorithmHKDF, AlgorithmInternal, _CryptoKey} from './CryptoKey';
import CryptoKey, {
Algorithm,
AlgorithmECDH,
AlgorithmECDSA,
AlgorithmHKDF,
AlgorithmInternal,
_CryptoKey
} from './CryptoKey';
import {allowOnlyKeys, allowOnlyValues, getBuffer, getCid, getNativeObject} from './util';
import checkType from './checkType';

Expand Down Expand Up @@ -75,11 +82,11 @@ class SubtleCrypto {
allowOnlyValues(format, ['spki', 'pkcs8', 'raw'], 'format');
checkType(getBuffer(keyData), ArrayBuffer, {name: 'keyData'});
if (typeof algorithm === 'string') {
allowOnlyValues(algorithm, ['ECDH', 'AES-GCM', 'HKDF'], 'algorithm');
allowOnlyValues(algorithm, ['AES-GCM', 'HKDF'], 'algorithm');
} else {
checkType(algorithm, Object, {name: 'algorithm'});
allowOnlyValues(algorithm.name, ['ECDH', 'AES-GCM'], 'algorithm.name');
if (algorithm.name === 'ECDH') {
allowOnlyValues(algorithm.name, ['ECDH', 'ECDSA', 'AES-GCM'], 'algorithm.name');
if (algorithm.name === 'ECDH' || algorithm.name === 'ECDSA') {
allowOnlyKeys(algorithm, ['name', 'namedCurve']);
allowOnlyValues(algorithm.namedCurve, ['P-256'], 'algorithm.namedCurve');
} else {
Expand Down Expand Up @@ -210,15 +217,15 @@ class SubtleCrypto {
}

async generateKey(
algorithm: AlgorithmECDH,
algorithm: AlgorithmECDH | AlgorithmECDSA,
extractable: boolean,
keyUsages: string[]
): Promise<{privateKey: CryptoKey, publicKey: CryptoKey}> {
if (arguments.length !== 3) {
throw new TypeError(`Expected 3 arguments, got ${arguments.length}`);
}
allowOnlyKeys(algorithm, ['name', 'namedCurve']);
allowOnlyValues(algorithm.name, ['ECDH'], 'algorithm.name');
allowOnlyValues(algorithm.name, ['ECDH', 'ECDSA'], 'algorithm.name');
allowOnlyValues(algorithm.namedCurve, ['P-256'], 'algorithm.namedCurve');
checkType(extractable, Boolean, {name: 'extractable'});
checkType(keyUsages, Array, {name: 'keyUsages'});
Expand Down
9 changes: 7 additions & 2 deletions src/tabris/CryptoKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {TypedArray} from './Crypto';
import NativeObject from './NativeObject';
import {getBuffer, getCid, setNativeObject} from './util';

export type AlgorithmInternal = AlgorithmHKDF | AlgorithmECDH | 'HKDF' | 'AES-GCM';
export type AlgorithmInternal = AlgorithmHKDF | AlgorithmECDH | AlgorithmECDSA | 'HKDF' | 'AES-GCM';

export type Algorithm = AlgorithmInternal | {name: 'AES-GCM'};

Expand All @@ -19,6 +19,11 @@ export type AlgorithmECDH = {
public?: CryptoKey
};

export type AlgorithmECDSA = {
name: 'ECDSA',
namedCurve: 'P-256'
};

export default class CryptoKey {

constructor(nativeObject: _CryptoKey, data: CryptoKey) {
Expand Down Expand Up @@ -107,7 +112,7 @@ export class _CryptoKey extends NativeObject {
}

async generate(
algorithm: AlgorithmECDH,
algorithm: AlgorithmECDH | AlgorithmECDSA,
extractable: boolean,
keyUsages: string[]
): Promise<void> {
Expand Down
12 changes: 10 additions & 2 deletions test/tabris/Crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ describe('Crypto', function() {
it('checks algorithm.name', async function() {
(params[2] as any).name = 'foo';
await expect(importKey())
.rejectedWith(TypeError, 'algorithm.name must be "ECDH" or "AES-GCM", got "foo"');
.rejectedWith(TypeError, 'algorithm.name must be "ECDH", "ECDSA" or "AES-GCM", got "foo"');
expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0);
});

Expand All @@ -665,6 +665,14 @@ describe('Crypto', function() {
expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0);
});

it('checks algorithm keys for ECDSA', async function() {
(params[2] as any).name = 'ECDSA';
(params[2] as any).foo = 'foo';
await expect(importKey())
.rejectedWith(TypeError, 'Object contains unexpected entry "foo"');
expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0);
});

it('checks algorithm keys for AES-GCM', async function() {
(params[2] as any) = {name: 'AES-GCM', namedCurve: 'P-256'};
await expect(importKey())
Expand Down Expand Up @@ -1069,7 +1077,7 @@ describe('Crypto', function() {
// @ts-ignore
params[0].name = 'foo';
await expect(generateKey())
.rejectedWith(TypeError, 'algorithm.name must be "ECDH", got "foo"');
.rejectedWith(TypeError, 'algorithm.name must be "ECDH" or "ECDSA", got "foo"');
expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0);
});

Expand Down

0 comments on commit 295fac2

Please sign in to comment.