Skip to content

Commit

Permalink
Support signing and verification using ECDSA
Browse files Browse the repository at this point in the history
Introduce the methods `crypto.subtle.sign()` and
`crypto.subtle.verify()` to create and verify ECDSA signatures.

Currently, only the combination of the algorithm `ECDSAinDERFormat` and
`SHA-256` hash is supported.

The algorithm `ECDSAinDERFormat` is similar to the `ECDSA` algorithm
used in `SubtleCrypto`, with the distinction that signature is encoded
in the DER format, diverging from the IEEE-P1363 format used in
`SubtleCrypto`.
  • Loading branch information
cpetrov committed Dec 6, 2023
1 parent 295fac2 commit 18aa66b
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 0 deletions.
83 changes: 83 additions & 0 deletions doc/api/SubtleCrypto.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,89 @@
"ArrayBuffer"
]
}
},
"sign": {
"description": "Signs the given data. Currently only supports creating ECDSA signatures in DER format.",
"parameters": [
{
"name": "algorithm",
"type": {
"map": {
"name": {
"type": "'ECDSAinDERFormat'"
},
"hash": {
"type": "'SHA-256'"
}
}
}
},
{
"name": "key",
"type": "CryptoKey"
},
{
"name": "data",
"type": {
"union": [
"ArrayBuffer",
"TypedArray"
]
}
}
],
"returns": {
"interface": "Promise",
"generics": [
"ArrayBuffer"
]
}
},
"verify": {
"description": "Verifies the given signature against the data. Currently only supports verifying ECDSA signatures in DER format.",
"parameters": [
{
"name": "algorithm",
"type": {
"map": {
"name": {
"type": "'ECDSAinDERFormat'"
},
"hash": {
"type": "'SHA-256'"
}
}
}
},
{
"name": "key",
"type": "CryptoKey"
},
{
"name": "signature",
"type": {
"union": [
"ArrayBuffer",
"TypedArray"
]
}
},
{
"name": "data",
"type": {
"union": [
"ArrayBuffer",
"TypedArray"
]
}
}
],
"returns": {
"interface": "Promise",
"generics": [
"boolean"
]
}
}
}
}
33 changes: 33 additions & 0 deletions snippets/crypto-sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {contentView, crypto, Stack, tabris, TextView} from 'tabris';

const stack = Stack({stretch: true, spacing: 8, padding: 16, alignment: 'stretchX'})
.appendTo(contentView);
tabris.onLog(({message}) => stack.append(TextView({text: message})));

(async function() {

// Generate a key pair for signing and verifying
const keyPair = await crypto.subtle.generateKey(
{name: 'ECDSA', namedCurve: 'P-256'},
true,
['sign', '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
);
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
);
console.log('Signature valid:', isValid);

}());
76 changes: 76 additions & 0 deletions src/tabris/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import checkType from './checkType';
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray
| Int16Array | Uint16Array | Int32Array | Uint32Array;

type SignatureAlgorithm = { name: 'ECDSAinDERFormat', hash: 'SHA-256' };

export default class Crypto {

readonly subtle!: SubtleCrypto;
Expand Down Expand Up @@ -239,6 +241,46 @@ class SubtleCrypto {
};
}

async sign(
algorithm: SignatureAlgorithm,
key: CryptoKey,
data: ArrayBuffer | TypedArray
): Promise<ArrayBuffer> {
if (arguments.length !== 3) {
throw new TypeError(`Expected 3 arguments, got ${arguments.length}`);
}
allowOnlyKeys(algorithm, ['name', 'hash']);
allowOnlyValues(algorithm.name, ['ECDSAinDERFormat'], 'algorithm.name');
allowOnlyValues(algorithm.hash, ['SHA-256'], 'algorithm.hash');
checkType(key, CryptoKey, {name: 'key'});
checkType(algorithm.name, String, {name: 'algorithm.name'});
checkType(algorithm.hash, String, {name: 'algorithm.hash'});
checkType(getBuffer(data), ArrayBuffer, {name: 'data'});
return new Promise((onSuccess, onError) =>
this._nativeObject.subtleSign(algorithm, key, data, onSuccess, onError)
);
}

async verify(
algorithm: SignatureAlgorithm,
key: CryptoKey,
signature: ArrayBuffer | TypedArray,
data: ArrayBuffer | TypedArray
): Promise<boolean> {
if (arguments.length !== 4) {
throw new TypeError(`Expected 4 arguments, got ${arguments.length}`);
}
allowOnlyKeys(algorithm, ['name', 'hash']);
allowOnlyValues(algorithm.name, ['ECDSAinDERFormat'], 'algorithm.name');
allowOnlyValues(algorithm.hash, ['SHA-256'], 'algorithm.hash');
checkType(key, CryptoKey, {name: 'key'});
checkType(getBuffer(signature), ArrayBuffer, {name: 'signature'});
checkType(getBuffer(data), ArrayBuffer, {name: 'data'});
return new Promise((onSuccess, onReject) =>
this._nativeObject.subtleVerify(algorithm, key, signature, data, onSuccess, onReject)
);
}

}

class NativeCrypto extends NativeObject {
Expand Down Expand Up @@ -351,6 +393,40 @@ class NativeCrypto extends NativeObject {
});
}

subtleSign(
algorithm: SignatureAlgorithm,
key: CryptoKey,
data: ArrayBuffer | TypedArray,
onSuccess: (buffer: ArrayBuffer) => any,
onError: (ex: Error) => any
): void {
this._nativeCall('subtleSign', {
algorithm,
key: getCid(key),
data: getBuffer(data),
onSuccess,
onError: (reason: unknown) => onError(new Error(String(reason)))
});
}

subtleVerify(
algorithm: SignatureAlgorithm,
key: CryptoKey,
signature: ArrayBuffer | TypedArray,
data: ArrayBuffer | TypedArray,
onSuccess: (isValid: boolean) => any,
onError: (ex: Error) => any
): void {
this._nativeCall('subtleVerify', {
algorithm,
key: getCid(key),
signature: getBuffer(signature),
data: getBuffer(data),
onSuccess,
onError: (reason: unknown) => onError(new Error(String(reason)))
});
}

}

function checkDeriveAlgorithm(algorithm: Algorithm):
Expand Down
169 changes: 169 additions & 0 deletions test/tabris/Crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1105,4 +1105,173 @@ describe('Crypto', function() {

});

describe('subtle.sign()', function() {
let params: Parameters<typeof crypto.subtle.sign>;
let data: ArrayBuffer;
let key: _CryptoKey;

async function sign(cb?: (nativeParams: any) => void) {
const promise = crypto.subtle.sign.apply(crypto.subtle, params);
cb?.call(null, client.calls({op: 'call', method: 'subtleSign'})[0].parameters);
return promise;
}

beforeEach(function() {
client.resetCalls();
data = new ArrayBuffer(10);
key = new _CryptoKey();
params = [
{name: 'ECDSAinDERFormat', hash: 'SHA-256'},
new CryptoKey(key, {}),
data
];
});

it('CALLs subtleSign', async function() {
await sign(param => param.onSuccess());
const signCalls = client.calls({op: 'call', method: 'subtleSign'});
expect(signCalls.length).to.equal(1);
expect(signCalls[0].parameters).to.deep.include({
algorithm: {name: 'ECDSAinDERFormat', hash: 'SHA-256'},
key: key.cid,
data
});
});

it('returns signed data', async function() {
const signed = new ArrayBuffer(2);
expect(await sign(param => param.onSuccess(signed)))
.to.equal(signed);
});

it('propagates rejection', async function() {
await expect(sign(param => param.onError('signerror')))
.rejectedWith(Error, 'signerror');
});

it('checks parameter length', async function() {
params.pop();
await expect(sign())
.rejectedWith(TypeError, 'Expected 3 arguments, got 2');
expect(client.calls({op: 'call', method: 'subtleSign'}).length).to.equal(0);
});

it('checks algorithm.name', async function() {
(params[0].name as any) = 'foo';
await expect(sign())
.rejectedWith(TypeError, 'algorithm.name must be "ECDSAinDERFormat", got "foo"');
expect(client.calls({op: 'call', method: 'subtleSign'}).length).to.equal(0);
});

it('checks algorithm.hash', async function() {
(params[0].hash as any) = 'foo';
await expect(sign())
.rejectedWith(TypeError, 'algorithm.hash must be "SHA-256", got "foo"');
expect(client.calls({op: 'call', method: 'subtleSign'}).length).to.equal(0);
});

it('checks key', async function() {
params[1] = null;
await expect(sign())
.rejectedWith(TypeError, 'Expected key to be of type CryptoKey, got null');
expect(client.calls({op: 'call', method: 'subtleSign'}).length).to.equal(0);
});

it('checks data', async function() {
params[2] = null;
await expect(sign())
.rejectedWith(TypeError, 'Expected data to be of type ArrayBuffer, got null');
expect(client.calls({op: 'call', method: 'subtleSign'}).length).to.equal(0);
});
});

describe('subtle.verify()', function() {
let params: Parameters<typeof crypto.subtle.verify>;
let signature: ArrayBuffer;
let data: ArrayBuffer;
let key: _CryptoKey;

async function verify(cb?: (nativeParams: any) => void) {
const promise = crypto.subtle.verify.apply(crypto.subtle, params);
cb?.call(null, client.calls({op: 'call', method: 'subtleVerify'})[0].parameters);
return promise;
}

beforeEach(function() {
client.resetCalls();
signature = new ArrayBuffer(2);
data = new ArrayBuffer(10);
key = new _CryptoKey();
params = [
{name: 'ECDSAinDERFormat', hash: 'SHA-256'},
new CryptoKey(key, {}),
signature,
data
];
});

it('CALLs subtleVerify', async function() {
await verify(param => param.onSuccess(true));
const verifyCalls = client.calls({op: 'call', method: 'subtleVerify'});
expect(verifyCalls.length).to.equal(1);
expect(verifyCalls[0].parameters).to.deep.include({
algorithm: {name: 'ECDSAinDERFormat', hash: 'SHA-256'},
key: key.cid,
signature,
data
});
});

it('returns verification result', async function() {
expect(await verify(param => param.onSuccess(true))).to.be.true;
});

it('propagates rejection', async function() {
await expect(verify(param => param.onError('verifyerror')))
.rejectedWith(Error, 'verifyerror');
});

it('checks parameter length', async function() {
params.pop();
await expect(verify())
.rejectedWith(TypeError, 'Expected 4 arguments, got 3');
expect(client.calls({op: 'call', method: 'subtleVerify'}).length).to.equal(0);
});

it('checks algorithm.name', async function() {
(params[0].name as any) = 'foo';
await expect(verify())
.rejectedWith(TypeError, 'algorithm.name must be "ECDSAinDERFormat", got "foo"');
expect(client.calls({op: 'call', method: 'subtleVerify'}).length).to.equal(0);
});

it('checks algorithm.hash', async function() {
(params[0].hash as any) = 'foo';
await expect(verify())
.rejectedWith(TypeError, 'algorithm.hash must be "SHA-256", got "foo"');
expect(client.calls({op: 'call', method: 'subtleVerify'}).length).to.equal(0);
});

it('checks key', async function() {
params[1] = null;
await expect(verify())
.rejectedWith(TypeError, 'Expected key to be of type CryptoKey, got null');
expect(client.calls({op: 'call', method: 'subtleVerify'}).length).to.equal(0);
});

it('checks signature', async function() {
params[2] = null;
await expect(verify())
.rejectedWith(TypeError, 'Expected signature to be of type ArrayBuffer, got null');
expect(client.calls({op: 'call', method: 'subtleVerify'}).length).to.equal(0);
});

it('checks data', async function() {
params[3] = null;
await expect(verify())
.rejectedWith(TypeError, 'Expected data to be of type ArrayBuffer, got null');
expect(client.calls({op: 'call', method: 'subtleVerify'}).length).to.equal(0);
});
});

});

0 comments on commit 18aa66b

Please sign in to comment.