Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
crypto: add subtle.getPublicKey() utility function in Web Cryptography
  • Loading branch information
panva committed Aug 18, 2025
commit 71bb1bb04f607c872b3c7f54760febf24eb845c6
78 changes: 47 additions & 31 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Key Formats:

Methods:

* [`subtle.getPublicKey()`][]
* [`SubtleCrypto.supports()`][]

## Secure Curves in the Web Cryptography API
Expand Down Expand Up @@ -478,36 +479,36 @@ const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt(
The table details the algorithms supported by the Node.js Web Crypto API
implementation and the APIs supported for each:

| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` |
| ---------------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- |
| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | |
| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | |
| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | |
| `'AES-KW'` | ✔ | ✔ | ✔ | | | ✔ | ✔ | | | | | |
| `'cSHAKE128'`[^modern-algos] | | | | | | | | | | | | ✔ |
| `'cSHAKE256'`[^modern-algos] | | | | | | | | | | | | ✔ |
| `'ECDH'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | |
| `'ECDSA'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'Ed25519'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'HKDF'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | |
| `'HMAC'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'PBKDF2'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | |
| `'SHA-1'` | | | | | | | | | | | | ✔ |
| `'SHA-256'` | | | | | | | | | | | | ✔ |
| `'SHA-384'` | | | | | | | | | | | | ✔ |
| `'SHA-512'` | | | | | | | | | | | | ✔ |
| `'SHA3-256'`[^modern-algos] | | | | | | | | | | | | ✔ |
| `'SHA3-384'`[^modern-algos] | | | | | | | | | | | | ✔ |
| `'SHA3-512'`[^modern-algos] | | | | | | | | | | | | ✔ |
| `'X25519'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | |
| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | |
| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` | `getPublicKey` |
| ---------------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- | -------------- |
| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | |
| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | |
| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | |
| `'AES-KW'` | ✔ | ✔ | ✔ | | | ✔ | ✔ | | | | | | |
| `'cSHAKE128'`[^modern-algos] | | | | | | | | | | | | ✔ | |
| `'cSHAKE256'`[^modern-algos] | | | | | | | | | | | | ✔ | |
| `'ECDH'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ |
| `'ECDSA'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'Ed25519'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'HKDF'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | |
| `'HMAC'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | |
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'PBKDF2'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | ✔ |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ |
| `'SHA-1'` | | | | | | | | | | | | ✔ | |
| `'SHA-256'` | | | | | | | | | | | | ✔ | |
| `'SHA-384'` | | | | | | | | | | | | ✔ | |
| `'SHA-512'` | | | | | | | | | | | | ✔ | |
| `'SHA3-256'`[^modern-algos] | | | | | | | | | | | | ✔ | |
| `'SHA3-384'`[^modern-algos] | | | | | | | | | | | | ✔ | |
| `'SHA3-512'`[^modern-algos] | | | | | | | | | | | | ✔ | |
| `'X25519'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ |
| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ |

## Class: `Crypto`

Expand Down Expand Up @@ -692,7 +693,7 @@ added: REPLACEME

<!--lint disable maximum-line-length remark-lint-->

* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "wrapKey", or "unwrapKey"
* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "getPublicKey", "wrapKey", or "unwrapKey"
* `algorithm` {string|Algorithm}
* `lengthOrAdditionalAlgorithm` {null|number|string|Algorithm|undefined} Depending on the operation this is either ignored, the value of the length argument when operation is "deriveBits", the algorithm of key to be derived when operation is "deriveKey", the algorithm of key to be exported before wrapping when operation is "wrapKey", or the algorithm of key to be imported after unwrapping when operation is "unwrapKey". **Default:** `null` when operation is "deriveBits", `undefined` otherwise.
* Returns: {boolean} Indicating whether the implementation supports the given operation
Expand Down Expand Up @@ -926,6 +927,20 @@ specification.
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | |

### `subtle.getPublicKey(key, keyUsages)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development

* `key` {CryptoKey} A private key from which to derive the corresponding public key.
* `keyUsages` {string\[]} See [Key usages][].
* Returns: {Promise} Fulfills with a {CryptoKey} upon success.

Derives the public key from a given private key.

### `subtle.generateKey(algorithm, extractable, keyUsages)`

<!-- YAML
Expand Down Expand Up @@ -2143,3 +2158,4 @@ The length (in bytes) of the random salt to use.
[Secure Curves in the Web Cryptography API]: #secure-curves-in-the-web-cryptography-api
[Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/
[`SubtleCrypto.supports()`]: #static-method-subtlecryptosupportsoperation-algorithm-lengthoradditionalalgorithm
[`subtle.getPublicKey()`]: #subtlegetpublickeykey-keyusages
55 changes: 55 additions & 0 deletions lib/internal/crypto/webcrypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
ReflectApply,
ReflectConstruct,
StringPrototypeRepeat,
StringPrototypeSlice,
SymbolToStringTag,
} = primordials;

Expand All @@ -29,6 +30,7 @@ const {
} = require('internal/errors');

const {
createPublicKey,
CryptoKey,
importGenericSecretKey,
} = require('internal/crypto/keys');
Expand Down Expand Up @@ -1028,6 +1030,31 @@ async function decrypt(algorithm, key, data) {
return cipherOrWrap(kWebCryptoCipherDecrypt, algorithm, key, data, 'decrypt');
}

// Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-getPublicKey
async function getPublicKey(key, keyUsages) {
emitExperimentalWarning('The getPublicKey Web Crypto API method');
if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto');

webidl ??= require('internal/crypto/webidl');
const prefix = "Failed to execute 'getPublicKey' on 'SubtleCrypto'";
webidl.requiredArguments(arguments.length, 2, { prefix });
key = webidl.converters.CryptoKey(key, {
prefix,
context: '1st argument',
});
keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages, {
prefix,
context: '2nd argument',
});

if (key.type !== 'private')
throw lazyDOMException('key must be a private key', 'InvalidAccessError');

const keyObject = createPublicKey(key[kKeyObject]);

return keyObject.toCryptoKey(key.algorithm, true, keyUsages);
}

// The SubtleCrypto and Crypto classes are defined as part of the
// Web Crypto API standard: https://www.w3.org/TR/WebCryptoAPI/

Expand Down Expand Up @@ -1066,6 +1093,7 @@ class SubtleCrypto {
case 'exportKey':
case 'wrapKey':
case 'unwrapKey':
case 'getPublicKey':
break;
default:
return false;
Expand Down Expand Up @@ -1116,6 +1144,26 @@ class SubtleCrypto {
context: '3rd argument',
});
}
} else if (operation === 'getPublicKey') {
let normalizedAlgorithm;
try {
normalizedAlgorithm = normalizeAlgorithm(algorithm, 'exportKey');
} catch {
return false;
}

switch (StringPrototypeSlice(normalizedAlgorithm.name, 0, 2)) {
case 'ML': // ML-DSA-*, ML-KEM-*
case 'SL': // SLH-DSA-*
case 'RS': // RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5
case 'EC': // ECDSA, ECDH
case 'Ed': // Ed*
case 'X2': // X25519
case 'X4': // X448
return true;
default:
return false;
}
}

return check(operation, algorithm, length);
Expand Down Expand Up @@ -1319,6 +1367,13 @@ ObjectDefineProperties(
writable: true,
value: unwrapKey,
},
getPublicKey: {
__proto__: null,
enumerable: true,
configurable: true,
writable: true,
value: getPublicKey,
},
});

module.exports = {
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/webcrypto/supports-modern-algorithms.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,20 @@ export const vectors = {
[pqc, 'ML-DSA-65'],
[pqc, 'ML-DSA-87'],
],
'getPublicKey': [
[true, 'RSA-OAEP'],
[true, 'RSA-PSS'],
[true, 'RSASSA-PKCS1-v1_5'],
[true, 'X25519'],
[true, 'X448'],
[true, 'Ed25519'],
[true, 'Ed448'],
[true, 'ECDH'],
[true, 'ECDSA'],
[pqc, 'ML-DSA-44'],
[false, 'AES-CTR'],
[false, 'AES-CBC'],
[false, 'AES-GCM'],
[false, 'AES-KW'],
],
};
7 changes: 7 additions & 0 deletions test/parallel/test-webcrypto-constructors.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ const notSubtle = Reflect.construct(function() {}, [], SubtleCrypto);
});
}

// Test SubtleCrypto.prototype.getPublicKey
{
assert.rejects(() => notSubtle.getPublicKey(), {
name: 'TypeError', code: 'ERR_INVALID_THIS',
}).then(common.mustCall());
}

{
subtle.importKey(
'raw',
Expand Down
51 changes: 51 additions & 0 deletions test/parallel/test-webcrypto-get-public-key.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as common from '../common/index.mjs';

if (!common.hasCrypto) common.skip('missing crypto');

import * as assert from 'node:assert';
const { subtle } = globalThis.crypto;

const RSA_KEY_GEN = {
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
};

const publicUsages = {
'ECDH': [],
'ECDSA': ['verify'],
'Ed25519': ['verify'],
'RSA-OAEP': ['encrypt', 'wrapKey'],
'RSA-PSS': ['verify'],
'RSASSA-PKCS1-v1_5': ['verify'],
'X25519': [],
};

for await (const { privateKey } of [
subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']),
subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']),
subtle.generateKey('Ed25519', false, ['sign']),
subtle.generateKey({ name: 'RSA-OAEP', ...RSA_KEY_GEN }, false, ['decrypt', 'unwrapKey']),
subtle.generateKey({ name: 'RSA-PSS', ...RSA_KEY_GEN }, false, ['sign']),
subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', ...RSA_KEY_GEN }, false, ['sign']),
subtle.generateKey('X25519', false, ['deriveBits']),
]) {
const { name } = privateKey.algorithm;
const usages = publicUsages[name];
const publicKey = await subtle.getPublicKey(privateKey, usages);
assert.deepStrictEqual(publicKey.algorithm, privateKey.algorithm);
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(publicKey.extractable, true);

await assert.rejects(() => subtle.getPublicKey(privateKey, ['deriveBits']), {
name: 'SyntaxError',
message: /Unsupported key usage/
});
}

const secretKey = await subtle.generateKey(
{ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']);
await assert.rejects(() => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), {
name: 'InvalidAccessError',
message: 'key must be a private key'
});