Merge pull request #1829

Re-enable using WebCrypto for X25519 when available.
This commit is contained in:
larabr 2025-03-05 11:51:40 +01:00 committed by GitHub
commit 1848f51a4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 24 deletions

View File

@ -106,6 +106,7 @@ jobs:
npx playwright install --with-deps firefox npx playwright install --with-deps firefox
- name: Install WebKit # caching not possible, external shared libraries required - name: Install WebKit # caching not possible, external shared libraries required
if: ${{ matrix.runner == 'macos-latest' }} # do not install on ubuntu, since the X25519 WebCrypto implementation has issues
run: npx playwright install --with-deps webkit run: npx playwright install --with-deps webkit
- name: Run browser tests - name: Run browser tests

View File

@ -11,6 +11,7 @@ import enums from '../../../enums';
import util from '../../../util'; import util from '../../../util';
import computeHKDF from '../../hkdf'; import computeHKDF from '../../hkdf';
import { getCipherParams } from '../../cipher'; import { getCipherParams } from '../../cipher';
import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64';
const HKDF_INFO = { const HKDF_INFO = {
x25519: util.encodeUTF8('OpenPGP X25519'), x25519: util.encodeUTF8('OpenPGP X25519'),
@ -24,7 +25,22 @@ const HKDF_INFO = {
*/ */
export async function generate(algo) { export async function generate(algo) {
switch (algo) { switch (algo) {
case enums.publicKey.x25519: { case enums.publicKey.x25519:
try {
const webCrypto = util.getWebCrypto();
const webCryptoKey = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']);
const privateKey = await webCrypto.exportKey('jwk', webCryptoKey.privateKey);
const publicKey = await webCrypto.exportKey('jwk', webCryptoKey.publicKey);
return {
A: new Uint8Array(b64ToUint8Array(publicKey.x)),
k: b64ToUint8Array(privateKey.d)
};
} catch (err) {
if (err.name !== 'NotSupportedError') {
throw err;
}
// k stays in little-endian, unlike legacy ECDH over curve25519 // k stays in little-endian, unlike legacy ECDH over curve25519
const k = getRandomBytes(32); const k = getRandomBytes(32);
const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k);
@ -171,7 +187,26 @@ export function getPayloadSize(algo) {
*/ */
export async function generateEphemeralEncryptionMaterial(algo, recipientA) { export async function generateEphemeralEncryptionMaterial(algo, recipientA) {
switch (algo) { switch (algo) {
case enums.publicKey.x25519: { case enums.publicKey.x25519:
try {
const webCrypto = util.getWebCrypto();
const jwk = publicKeyToJWK(algo, recipientA);
const ephemeralKeyPair = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']);
const recipientPublicKey = await webCrypto.importKey('jwk', jwk, 'X25519', false, []);
const sharedSecretBuffer = await webCrypto.deriveBits(
{ name: 'X25519', public: recipientPublicKey },
ephemeralKeyPair.privateKey,
getPayloadSize(algo) * 8 // in bits
);
const ephemeralPublicKeyJwt = await webCrypto.exportKey('jwk', ephemeralKeyPair.publicKey);
return {
sharedSecret: new Uint8Array(sharedSecretBuffer),
ephemeralPublicKey: new Uint8Array(b64ToUint8Array(ephemeralPublicKeyJwt.x))
};
} catch (err) {
if (err.name !== 'NotSupportedError') {
throw err;
}
const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo)); const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo));
const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA);
assertNonZeroArray(sharedSecret); assertNonZeroArray(sharedSecret);
@ -193,7 +228,23 @@ export async function generateEphemeralEncryptionMaterial(algo, recipientA) {
export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) { export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) {
switch (algo) { switch (algo) {
case enums.publicKey.x25519: { case enums.publicKey.x25519:
try {
const webCrypto = util.getWebCrypto();
const privateKeyJWK = privateKeyToJWK(algo, A, k);
const ephemeralPublicKeyJWK = publicKeyToJWK(algo, ephemeralPublicKey);
const privateKey = await webCrypto.importKey('jwk', privateKeyJWK, 'X25519', false, ['deriveKey', 'deriveBits']);
const ephemeralPublicKeyReference = await webCrypto.importKey('jwk', ephemeralPublicKeyJWK, 'X25519', false, []);
const sharedSecretBuffer = await webCrypto.deriveBits(
{ name: 'X25519', public: ephemeralPublicKeyReference },
privateKey,
getPayloadSize(algo) * 8 // in bits
);
return new Uint8Array(sharedSecretBuffer);
} catch (err) {
if (err.name !== 'NotSupportedError') {
throw err;
}
const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey); const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey);
assertNonZeroArray(sharedSecret); assertNonZeroArray(sharedSecret);
return sharedSecret; return sharedSecret;
@ -224,3 +275,32 @@ function assertNonZeroArray(sharedSecret) {
throw new Error('Unexpected low order point'); throw new Error('Unexpected low order point');
} }
} }
function publicKeyToJWK(algo, publicKey) {
switch (algo) {
case enums.publicKey.x25519: {
const jwk = {
kty: 'OKP',
crv: 'X25519',
x: uint8ArrayToB64(publicKey, true),
ext: true
};
return jwk;
}
default:
throw new Error('Unsupported ECDH algorithm');
}
}
function privateKeyToJWK(algo, publicKey, privateKey) {
switch (algo) {
case enums.publicKey.x25519: {
const jwk = publicKeyToJWK(algo, publicKey);
jwk.d = uint8ArrayToB64(privateKey, true);
return jwk;
}
default:
throw new Error('Unsupported ECDH algorithm');
}
}

View File

@ -234,9 +234,9 @@ export default () => describe('ECDH key exchange @lightweight', function () {
for (const { vector } of vectors) { for (const { vector } of vectors) {
const lowOrderPoint = util.hexToUint8Array(vector); const lowOrderPoint = util.hexToUint8Array(vector);
const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x25519); const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x25519);
await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x25519, data, lowOrderPoint)).to.be.rejectedWith(/low order point/); await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x25519, data, lowOrderPoint)).to.be.rejected; // OperationError, DataError or 'low order point', depending on platform
const dummyWrappedKey = new Uint8Array(32); // expected to be unused const dummyWrappedKey = new Uint8Array(32); // expected to be unused
await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x25519, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/low order point/); await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x25519, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejected; // OperationError, DataError or 'low order point', depending on platform
} }
}); });

View File

@ -1,10 +1,10 @@
import { playwrightLauncher } from '@web/test-runner-playwright'; import { existsSync } from 'fs';
import { playwrightLauncher, playwright } from '@web/test-runner-playwright';
const sharedPlaywrightCIOptions = { const sharedPlaywrightCIOptions = {
// createBrowserContext: ({ browser }) => browser.newContext({ ignoreHTTPSErrors: true }), // createBrowserContext: ({ browser }) => browser.newContext({ ignoreHTTPSErrors: true }),
headless: true headless: true
}; };
export default { export default {
nodeResolve: true, // to resolve npm module imports in `unittests.html` nodeResolve: true, // to resolve npm module imports in `unittests.html`
files: './test/unittests.html', files: './test/unittests.html',
@ -29,11 +29,13 @@ export default {
...sharedPlaywrightCIOptions, ...sharedPlaywrightCIOptions,
product: 'firefox' product: 'firefox'
}), }),
playwrightLauncher({ // try setting up webkit, but ignore if not available
// (e.g. on ubuntu, where we don't want to test webkit as the WebCrypto X25519 implementation has issues)
existsSync(playwright.webkit.executablePath()) && playwrightLauncher({
...sharedPlaywrightCIOptions, ...sharedPlaywrightCIOptions,
product: 'webkit' product: 'webkit'
}) })
] ].filter(Boolean)
} }
] ]
}; };