diff --git a/doc/api/SubtleCrypto.json b/doc/api/SubtleCrypto.json
index 7fb5b678..24a87276 100644
--- a/doc/api/SubtleCrypto.json
+++ b/doc/api/SubtleCrypto.json
@@ -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"
+        ]
+      }
     }
   }
 }
diff --git a/snippets/crypto-sign.ts b/snippets/crypto-sign.ts
new file mode 100644
index 00000000..788f41e4
--- /dev/null
+++ b/snippets/crypto-sign.ts
@@ -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);
+
+}());
diff --git a/src/tabris/Crypto.ts b/src/tabris/Crypto.ts
index 873dd06e..0726b345 100644
--- a/src/tabris/Crypto.ts
+++ b/src/tabris/Crypto.ts
@@ -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;
@@ -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 {
@@ -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):
diff --git a/test/tabris/Crypto.test.ts b/test/tabris/Crypto.test.ts
index 0b6572d1..492befb2 100644
--- a/test/tabris/Crypto.test.ts
+++ b/test/tabris/Crypto.test.ts
@@ -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);
+    });
+  });
+
 });