Skip to content

Commit

Permalink
feat(cloudflare workers): add support for EdDSA using Ed25519
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Sep 10, 2021
1 parent 82fa773 commit 0967369
Show file tree
Hide file tree
Showing 14 changed files with 106 additions and 14 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Legend:
| -- | -- | -- |
| Node.js | LTS ^12.19.0 || ^14.15.0 | |
| Electron | ^12.0.0 | see <sup>[1]</sup> |
| Cloudflare Workers || see <sup>[2], [4]</sup> |
| Cloudflare Workers || see <sup>[2], [5]</sup> |
| Deno | experimental | see Deno's [Web Cryptography API roadmap](https://github.com/denoland/deno/issues/11690) |
| React Native || has no available and usable crypto runtime |
| IE || implements old version of the Web Cryptography API specification |
Expand All @@ -163,6 +163,8 @@ Legend:

<sup>4</sup> 192 bit AES keys are not supported in Chromium

<sup>5</sup> OKP / EdDSA / Ed25519 is supported

## FAQ

#### Supported Versions
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/browser/fetch_jwks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const fetchJwks: FetchFunction = async (url: URL, timeout: number) => {
// do not pass referrerPolicy, credentials, and mode when running
// in Cloudflare Workers environment
// @ts-expect-error
...(typeof globalThis.WebSocketPair === 'undefined'
...(globalThis.WebSocketPair === undefined
? {
referrerPolicy: 'no-referrer',
credentials: 'omit',
Expand Down
18 changes: 18 additions & 0 deletions src/runtime/browser/generate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCloudflareWorkers, isNodeJs } from './global.js'
import crypto from './webcrypto.js'
import { JOSENotSupported } from '../../util/errors.js'
import random from './random.js'
Expand Down Expand Up @@ -109,6 +110,23 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti
algorithm = { name: 'ECDSA', namedCurve: 'P-521' }
keyUsages = ['sign', 'verify']
break
case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA':
switch (options?.crv) {
case undefined:
case 'Ed25519':
algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' }
keyUsages = ['sign', 'verify']
break
case isNodeJs() && 'Ed448':
algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' }
keyUsages = ['sign', 'verify']
break
default:
throw new JOSENotSupported(
'Invalid or unsupported crv option provided, supported values are Ed25519 and Ed448',
)
}
break
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/browser/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,19 @@ function getGlobal() {
}

export default getGlobal()
export function isCloudflareWorkers(): boolean {
try {
// @ts-expect-error
return getGlobal().WebSocketPair !== undefined
} catch {
return false
}
}
export function isNodeJs(): boolean {
try {
// @deno-expect-error
return getGlobal().process?.versions?.node !== undefined
} catch {
return false
}
}
28 changes: 27 additions & 1 deletion src/runtime/browser/jwk_to_key.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCloudflareWorkers, isNodeJs } from './global.js'
import crypto from './webcrypto.js'
import type { JWKParseFunction } from '../interfaces.d'
import { JOSENotSupported } from '../../util/errors.js'
Expand Down Expand Up @@ -84,9 +85,15 @@ function subtleMapping(jwk: JWK): {
case 'EC': {
switch (jwk.alg) {
case 'ES256':
algorithm = { name: 'ECDSA', namedCurve: 'P-256' }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
case 'ES384':
algorithm = { name: 'ECDSA', namedCurve: 'P-384' }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
case 'ES512':
algorithm = { name: 'ECDSA', namedCurve: jwk.crv! }
algorithm = { name: 'ECDSA', namedCurve: 'P-521' }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
case 'ECDH-ES':
Expand All @@ -101,6 +108,25 @@ function subtleMapping(jwk: JWK): {
}
break
}
case (isCloudflareWorkers() || isNodeJs()) && 'OKP':
if (jwk.alg !== 'EdDSA') {
throw new JOSENotSupported('unsupported or invalid JWK "alg" (Algorithm) Parameter value')
}
switch (jwk.crv) {
case 'Ed25519':
algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
case isNodeJs() && 'Ed448':
algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
default:
throw new JOSENotSupported(
'unsupported or invalid JWK "crv" (Subtype of Key Pair) Parameter value',
)
}
break
default:
throw new JOSENotSupported('unsupported or invalid JWK "kty" (Key Type) Parameter value')
}
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/browser/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import getSignKey from './get_sign_verify_key.js'
const sign: SignFunction = async (alg, key: unknown, data) => {
const cryptoKey = await getSignKey(alg, key, 'sign')
checkKeyLength(alg, cryptoKey)
const signature = await crypto.subtle.sign(subtleAlgorithm(alg), cryptoKey, data)
const signature = await crypto.subtle.sign(
// @deno-expect-error
subtleAlgorithm(alg, (<EcKeyAlgorithm>cryptoKey.algorithm).namedCurve),
cryptoKey,
data,
)
return new Uint8Array(signature)
}

Expand Down
6 changes: 5 additions & 1 deletion src/runtime/browser/subtle_dsa.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isCloudflareWorkers, isNodeJs } from './global.js'
import { JOSENotSupported } from '../../util/errors.js'

export default function subtleDsa(alg: string) {
export default function subtleDsa(alg: string, crv?: string) {
switch (alg) {
case 'HS256':
return { hash: { name: 'SHA-256' }, name: 'HMAC' }
Expand Down Expand Up @@ -38,6 +39,9 @@ export default function subtleDsa(alg: string) {
return { hash: { name: 'SHA-384' }, name: 'ECDSA', namedCurve: 'P-384' }
case 'ES512':
return { hash: { name: 'SHA-512' }, name: 'ECDSA', namedCurve: 'P-521' }
case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA':
// @deno-expect-error
return <EcKeyAlgorithm>{ name: crv, namedCurve: crv }
default:
throw new JOSENotSupported(
`alg ${alg} is not supported either by JOSE or your javascript runtime`,
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/browser/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import getVerifyKey from './get_sign_verify_key.js'
const verify: VerifyFunction = async (alg, key: unknown, signature, data) => {
const cryptoKey = await getVerifyKey(alg, key, 'verify')
checkKeyLength(alg, cryptoKey)
const algorithm = subtleAlgorithm(alg)
// @deno-expect-error
const algorithm = subtleAlgorithm(alg, (<EcKeyAlgorithm>cryptoKey.algorithm).namedCurve)
try {
return await crypto.subtle.verify(algorithm, cryptoKey, signature, data)
} catch {
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/node/webcrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ export function getKeyObject(key: CryptoKey, alg?: string, usage?: Set<KeyUsage>
}
break
}
case 'EdDSA': {
if (key.algorithm.name !== 'NODE-ED25519' && key.algorithm.name !== 'NODE-ED448') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be NODE-ED25519 or NODE-ED448.`,
)
}
break
}
case 'ES256':
case 'ES384':
case 'ES512': {
Expand Down
12 changes: 12 additions & 0 deletions test-cloudflare-workers/cloudflare.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ test('ES512', macro, async () => {
await jwsAsymmetricTest(keypair, alg);
});

test('EdDSA', macro, async () => {
const alg = 'EdDSA';
const keypair = await utilGenerateKeyPair(alg);
await jwsAsymmetricTest(keypair, alg);
});

test('EdDSA crv: Ed25519', macro, async () => {
const alg = 'EdDSA';
const keypair = await utilGenerateKeyPair(alg, { crv: 'Ed25519' });
await jwsAsymmetricTest(keypair, alg);
});

test('createRemoteJWKSet', macro, async () => {
const jwksUri = 'https://www.googleapis.com/oauth2/v3/certs';
const response = await fetch(jwksUri).then((r) => r.json());
Expand Down
4 changes: 2 additions & 2 deletions test/jwk/jwk2key.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,14 @@ Promise.all([import(`${keyRoot}/jwk/parse`), import(`${keyRoot}/jwk/from_key_lik
d: 'FRaWZohbbDyzhYpTCS9m4fv2xoK6HG83bw6jq6zNxEs',
kty: 'OKP',
};
conditional({ webcrypto: 0 })(testKeyImportExport, { ...ed25519, alg: 'EdDSA' });
conditional({ webcrypto: 1 })(testKeyImportExport, { ...ed25519, alg: 'EdDSA' });
const ed448 = {
crv: 'Ed448',
x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA',
d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n',
kty: 'OKP',
};
conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' });
conditional({ webcrypto: 1, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' });
const x25519 = {
crv: 'X25519',
x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4',
Expand Down
2 changes: 1 addition & 1 deletion test/jws/cookbook.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Promise.all([
},
{
title: 'https://tools.ietf.org/html/rfc8037#appendix-A.4 - Ed25519 Signing',
webcrypto: false,
webcrypto: true,
reproducible: true,
input: {
payload: 'Example of Ed25519 signing',
Expand Down
4 changes: 2 additions & 2 deletions test/jws/smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ Promise.all([
}

conditional({ webcrypto: 0, electron: 0 })(smoke, 'secp256k1');
conditional({ webcrypto: 0 })(smoke, 'ed25519');
conditional({ webcrypto: 0, electron: 0 })(smoke, 'ed448');
conditional({ webcrypto: 1 })(smoke, 'ed25519');
conditional({ webcrypto: 1, electron: 0 })(smoke, 'ed448');
},
(err) => {
test('failed to import', (t) => {
Expand Down
6 changes: 3 additions & 3 deletions test/util/generators.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ Promise.all([
return run;
}

conditional({ webcrypto: 0 })(testKeyPair, 'EdDSA');
conditional({ webcrypto: 0 })('crv: Ed25519', testKeyPair, 'EdDSA', { crv: 'Ed25519' });
conditional({ webcrypto: 0, electron: 0 })('crv: Ed448', testKeyPair, 'EdDSA', {
conditional({ webcrypto: 1 })(testKeyPair, 'EdDSA');
conditional({ webcrypto: 1 })('crv: Ed25519', testKeyPair, 'EdDSA', { crv: 'Ed25519' });
conditional({ webcrypto: 1, electron: 0 })('crv: Ed448', testKeyPair, 'EdDSA', {
crv: 'Ed448',
});
conditional({ webcrypto: 0, electron: 0 })(testKeyPair, 'ES256K');
Expand Down

0 comments on commit 0967369

Please sign in to comment.